Skip to content

Commit f7c9f9c

Browse files
committed
feat(starters): add htmx+go and fix go starter
• Added the HTMX + Go starter with a Go-rendered page, HTMX fragment endpoint, refresh-button with swap, agent support, CLI metadata, starter archive wiring, docs updates, and HTMX logo • Fixed the existing Go starter by correcting bad Elements bundle URLs, loading CSS and JS from the right packages, removing the frontend dependency/setup path, fixing stale copy, updating source links, simplifying template data • More Go server functionality with PORT support, root-only routing, explicit HTML content type, and template execution error handling. • Go and HTMX+Go starters are freed from frontend build tooling by using browser CDN bundles, excluding local build/cache outputs, skipping Elements dependency setup, and stamping CDN bundle versions during archive generation. • Test updates including Elements version stamping logic Signed-off-by: Jake Guza <jguza@nvidia.com>
1 parent d01f774 commit f7c9f9c

34 files changed

Lines changed: 652 additions & 79 deletions

File tree

.github/actions/setup-ci/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ runs:
1313
node-version-file: './.nvmrc'
1414
registry-url: 'https://registry.npmjs.org'
1515
cache: 'pnpm'
16+
- uses: actions/setup-go@v6
17+
with:
18+
go-version: '1.26.x'
19+
cache: false
1620
- name: Install dependencies
1721
shell: bash
1822
run: pnpm install --frozen-lockfile

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ projects/starters/bundles/src/index.html
3030
projects/starters/eleventy/src/_layouts/index.11ty.js
3131
projects/starters/eleventy/src/index.11ty.js
3232
projects/starters/go/src/index.html
33+
projects/starters/go-htmx/src/index.html
3334
projects/starters/hugo/**/*
3435
projects/starters/importmaps/src/index.html
3536
projects/starters/mpa/src/**/*.html
@@ -39,4 +40,4 @@ projects/starters/solidjs/src/App.tsx
3940
projects/starters/typescript/src/index.html
4041
projects/starters/vue/src/**/*
4142
projects/starters/nuxt/.nuxt/**/*
42-
projects/starters/nuxt/app/**/*
43+
projects/starters/nuxt/app/**/*

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ Examples of projects include:
3333

3434
To set up repository dependencies and run the full build, run the following commands at the **root** of the repository:
3535

36+
The CI pipeline also builds Go starters. Install [Go 1.26.x](https://go.dev/doc/install) before running the full local CI pipeline.
37+
3638
```shell
3739
# install required dependencies
3840
brew install git-lfs
3941
git lfs install
4042
git lfs pull
43+
go version
4144
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
4245
. ~/.nvm/nvm.sh
4346
nvm install

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
"./projects/starters/bundles:ci",
8787
"./projects/starters/eleventy:ci",
8888
"./projects/starters/eleventy-ssr:ci",
89+
"./projects/starters/go:ci",
90+
"./projects/starters/go-htmx:ci",
8991
"./projects/starters/hugo:ci",
9092
"./projects/starters/importmaps:ci",
9193
"./projects/starters/lit-library:ci",

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ packages:
66
- projects/starters/eleventy
77
- projects/starters/eleventy-ssr
88
- projects/starters/go
9+
- projects/starters/go-htmx
910
- projects/starters/hugo
1011
- projects/starters/importmaps
1112
- projects/starters/lit-library

projects/internals/tools/src/project/service.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ vi.mock('./setup-agent.js', () => ({
1515
vi.mock('./starters.js', () => ({
1616
startersData: {
1717
typescript: { cli: true, zip: 'typescript.zip' },
18-
react: { cli: true, zip: 'react.zip' },
19-
importmaps: { cli: false, zip: 'importmaps.zip' }
18+
go: { cli: true, zip: 'go.zip', setupElementsDependencies: false },
19+
lit: { cli: false, zip: null }
2020
},
2121
createStarter: vi.fn(),
2222
startStarter: vi.fn()
@@ -60,6 +60,25 @@ describe('ProjectService', () => {
6060
expect(updateProject).toHaveBeenCalled();
6161
});
6262

63+
it('should skip dependency setup for starters that opt out of project setup', async () => {
64+
const { createStarter } = await import('./starters.js');
65+
const { setupAgent } = await import('./setup-agent.js');
66+
const { setupProject } = await import('./setup.js');
67+
const { updateProject } = await import('./update.js');
68+
vi.mocked(createStarter).mockResolvedValue({ create: { message: 'created', status: 'success' } });
69+
vi.mocked(setupAgent).mockResolvedValue({ agent: { message: 'configured', status: 'success' } });
70+
71+
const { ProjectService } = await import('./service.js');
72+
const result = await ProjectService.create({ type: 'go', cwd: '/test', start: false });
73+
74+
expect(result).toHaveProperty('create');
75+
expect(result).toHaveProperty('agent');
76+
expect(createStarter).toHaveBeenCalledWith('go', '/test');
77+
expect(setupAgent).toHaveBeenCalled();
78+
expect(setupProject).not.toHaveBeenCalled();
79+
expect(updateProject).not.toHaveBeenCalled();
80+
});
81+
6382
it('should return failed report when a step fails', async () => {
6483
const { createStarter } = await import('./starters.js');
6584
const { setupAgent } = await import('./setup-agent.js');

projects/internals/tools/src/project/service.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const starters = Object.keys(startersData).filter(
1616
starter => startersData[starter as keyof typeof startersData]?.cli
1717
) as Starter[];
1818

19+
function starterShouldSetupElementsDependencies(type: Starter): boolean {
20+
const starterData = startersData[type];
21+
return !('setupElementsDependencies' in starterData) || starterData.setupElementsDependencies;
22+
}
23+
1924
@service()
2025
export class ProjectService {
2126
@tool({
@@ -56,9 +61,12 @@ export class ProjectService {
5661

5762
const createReport = await createStarter(type, dir);
5863
const agentReport = await setupAgent(projectDir, 'all');
59-
const setupProjectReport = await setupProject(projectDir);
60-
const updateReport = await updateProject(projectDir);
61-
const reports = [createReport, agentReport, setupProjectReport, updateReport];
64+
const reports = [createReport, agentReport];
65+
if (starterShouldSetupElementsDependencies(type)) {
66+
const setupProjectReport = setupProject(projectDir);
67+
const updateProjectReport = await updateProject(projectDir);
68+
reports.push(setupProjectReport, updateProjectReport);
69+
}
6270

6371
const failedReport = reports.find(report => Object.values(report).some(value => value.status === 'danger'));
6472
if (failedReport) {
@@ -69,12 +77,7 @@ export class ProjectService {
6977
await startStarter(projectDir);
7078
}
7179

72-
return {
73-
...createReport,
74-
...agentReport,
75-
...setupProjectReport,
76-
...updateReport
77-
};
80+
return Object.assign({}, ...reports);
7881
}
7982

8083
@tool({

projects/internals/tools/src/project/starters.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import type * as childProcess from 'node:child_process';
55
import { beforeEach, describe, expect, it, vi } from 'vitest';
66
import {
77
startersData,
8+
createStarterCDNUrl,
89
createGitInitProcess,
910
createStarterPaths,
1011
execPackageManager,
1112
getDependencyInstallFailureMessage,
1213
getRequiredNPMClient,
14+
stampStarterCDNVersions,
1315
removeWireitScripts,
1416
startStarter
1517
} from './starters.js';
@@ -48,6 +50,7 @@ describe('startersData', () => {
4850
expect(startersData.nextjs.cli).toBe(true);
4951
expect(startersData.solidjs.cli).toBe(true);
5052
expect(startersData.go.cli).toBe(true);
53+
expect(startersData['go-htmx'].cli).toBe(true);
5154
expect(startersData.hugo.cli).toBe(true);
5255
expect(startersData.eleventy.cli).toBe(true);
5356
expect(startersData.bundles.cli).toBe(true);
@@ -66,6 +69,7 @@ describe('startersData', () => {
6669
expect(startersData.nextjs.zip).toContain('nextjs.zip');
6770
expect(startersData.solidjs.zip).toContain('solidjs.zip');
6871
expect(startersData.go.zip).toContain('go.zip');
72+
expect(startersData['go-htmx'].zip).toContain('go-htmx.zip');
6973
expect(startersData.eleventy.zip).toContain('eleventy.zip');
7074
expect(startersData.importmaps.zip).toContain('importmaps.zip');
7175
expect(startersData.bundles.zip).toContain('bundles.zip');
@@ -198,6 +202,96 @@ describe('getDependencyInstallFailureMessage', () => {
198202
});
199203
});
200204

205+
describe('starter CDN URLs', () => {
206+
type StarterCDNPackageName = Parameters<typeof createStarterCDNUrl>[0];
207+
type StarterCDNAsset = { packageName: StarterCDNPackageName; filePath: string };
208+
209+
const versions: Record<StarterCDNPackageName, string> = {
210+
'@nvidia-elements/core': '1.2.3',
211+
'@nvidia-elements/styles': '4.5.6',
212+
'@nvidia-elements/themes': '7.8.9'
213+
};
214+
215+
const coreAsset: StarterCDNAsset = { packageName: '@nvidia-elements/core', filePath: 'dist/bundles/index.min.js' };
216+
const stylesAsset: StarterCDNAsset = { packageName: '@nvidia-elements/styles', filePath: 'dist/bundles/index.css' };
217+
const themesAsset: StarterCDNAsset = { packageName: '@nvidia-elements/themes', filePath: 'dist/bundles/index.css' };
218+
const themeFontsAsset: StarterCDNAsset = { packageName: '@nvidia-elements/themes', filePath: 'dist/fonts/inter.css' };
219+
const starterCDNAssets = [stylesAsset, themesAsset, themeFontsAsset, coreAsset];
220+
221+
function createVersionedStarterCDNUrl(asset: StarterCDNAsset, version = versions[asset.packageName]) {
222+
return createStarterCDNUrl(asset.packageName, version, asset.filePath);
223+
}
224+
225+
function createUnversionedStarterCDNUrl(asset: StarterCDNAsset) {
226+
return createVersionedStarterCDNUrl(asset).replace(
227+
`${asset.packageName}@${versions[asset.packageName]}`,
228+
asset.packageName
229+
);
230+
}
231+
232+
it('should create versioned CDN URLs', () => {
233+
const url = createVersionedStarterCDNUrl(coreAsset);
234+
235+
expect(new URL(url).protocol).toBe('https:');
236+
expect(url).toContain(`${coreAsset.packageName}@${versions[coreAsset.packageName]}/${coreAsset.filePath}`);
237+
});
238+
239+
it('should stamp unversioned Elements CDN URLs with package versions', () => {
240+
const content = `
241+
<link rel="stylesheet" href="${createUnversionedStarterCDNUrl(stylesAsset)}" />
242+
<link rel="stylesheet" href="${createUnversionedStarterCDNUrl(themesAsset)}" />
243+
<link rel="stylesheet" href="${createUnversionedStarterCDNUrl(themeFontsAsset)}" />
244+
<script type="module">
245+
import '${createUnversionedStarterCDNUrl(coreAsset)}';
246+
</script>
247+
`;
248+
249+
const result = stampStarterCDNVersions(content, versions);
250+
251+
for (const asset of starterCDNAssets) {
252+
expect(result).toContain(createVersionedStarterCDNUrl(asset));
253+
}
254+
});
255+
256+
it('should replace existing Elements CDN versions', () => {
257+
const content = `
258+
<link rel="stylesheet" href="${createVersionedStarterCDNUrl(stylesAsset, '0.0.1')}" />
259+
<link rel="stylesheet" href="${createVersionedStarterCDNUrl(themesAsset, '0.0.1')}" />
260+
<link rel="stylesheet" href="${createVersionedStarterCDNUrl(themeFontsAsset, '0.0.1')}" />
261+
<script type="module">
262+
import '${createVersionedStarterCDNUrl(coreAsset, '0.0.1')}';
263+
</script>
264+
`;
265+
266+
const result = stampStarterCDNVersions(content, versions);
267+
268+
for (const asset of starterCDNAssets) {
269+
expect(result).toContain(createVersionedStarterCDNUrl(asset));
270+
}
271+
expect(result).not.toContain('@0.0.1/');
272+
});
273+
274+
it('should leave unrelated CDN URLs unchanged', () => {
275+
const unrelatedElementsPackageUrl = createVersionedStarterCDNUrl(coreAsset).replace(
276+
`${coreAsset.packageName}@${versions[coreAsset.packageName]}/${coreAsset.filePath}`,
277+
'@nvidia-elements/monaco/dist/bundles/index.css'
278+
);
279+
const htmxUrl = createVersionedStarterCDNUrl(coreAsset).replace(
280+
`${coreAsset.packageName}@${versions[coreAsset.packageName]}/${coreAsset.filePath}`,
281+
'htmx.org@2.0.10/dist/htmx.min.js'
282+
);
283+
const content = `
284+
<link rel="stylesheet" href="${unrelatedElementsPackageUrl}" />
285+
<script src="${htmxUrl}"></script>
286+
`;
287+
288+
const result = stampStarterCDNVersions(content, versions);
289+
290+
expect(result).toContain(unrelatedElementsPackageUrl);
291+
expect(result).toContain(htmxUrl);
292+
});
293+
});
294+
201295
describe('getNPMClient', () => {
202296
it('should return the npm client', async () => {
203297
expect(await getNPMClient()).toBe('pnpm');

0 commit comments

Comments
 (0)