Skip to content

Commit 524795b

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 59d8349 commit 524795b

32 files changed

Lines changed: 551 additions & 74 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: 'stable'
19+
cache: false
1620
- name: Install dependencies
1721
shell: bash
1822
run: pnpm install --frozen-lockfile

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: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ vi.mock('./setup-agent.js', () => ({
1414

1515
vi.mock('./starters.js', () => ({
1616
startersData: {
17-
typescript: { cli: true, zip: 'typescript.zip' },
18-
react: { cli: true, zip: 'react.zip' },
19-
importmaps: { cli: false, zip: 'importmaps.zip' }
17+
default: { cli: true, zip: 'default.zip' },
18+
static: { cli: true, zip: 'static.zip', setupElementsDependencies: false },
19+
hidden: { cli: false, zip: 'hidden.zip', setupElementsDependencies: false }
2020
},
2121
createStarter: vi.fn(),
2222
startStarter: vi.fn()
@@ -48,18 +48,37 @@ describe('ProjectService', () => {
4848
vi.mocked(updateProject).mockResolvedValue({ update: { message: 'updated', status: 'success' } });
4949

5050
const { ProjectService } = await import('./service.js');
51-
const result = await ProjectService.create({ type: 'typescript', cwd: '/test', start: false });
51+
const result = await ProjectService.create({ type: 'default', cwd: '/test', start: false });
5252

5353
expect(result).toHaveProperty('create');
5454
expect(result).toHaveProperty('agent');
5555
expect(result).toHaveProperty('deps');
5656
expect(result).toHaveProperty('update');
57-
expect(createStarter).toHaveBeenCalledWith('typescript', '/test');
57+
expect(createStarter).toHaveBeenCalledWith('default', '/test');
5858
expect(setupAgent).toHaveBeenCalled();
5959
expect(setupProject).toHaveBeenCalled();
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: 'static', cwd: '/test', start: false });
73+
74+
expect(result).toHaveProperty('create');
75+
expect(result).toHaveProperty('agent');
76+
expect(createStarter).toHaveBeenCalledWith('static', '/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');
@@ -73,7 +92,7 @@ describe('ProjectService', () => {
7392
vi.mocked(updateProject).mockResolvedValue({ update: { message: 'ok', status: 'success' } });
7493

7594
const { ProjectService } = await import('./service.js');
76-
const result = await ProjectService.create({ type: 'typescript', cwd: '/test', start: false });
95+
const result = await ProjectService.create({ type: 'default', cwd: '/test', start: false });
7796

7897
expect(result.create?.status).toBe('danger');
7998
});
@@ -90,7 +109,7 @@ describe('ProjectService', () => {
90109
vi.mocked(updateProject).mockResolvedValue({ update: { message: 'ok', status: 'success' } });
91110

92111
const { ProjectService } = await import('./service.js');
93-
await ProjectService.create({ type: 'typescript', cwd: '/test', start: true });
112+
await ProjectService.create({ type: 'default', cwd: '/test', start: true });
94113

95114
expect(startStarter).toHaveBeenCalled();
96115
});

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

Lines changed: 10 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,10 @@ 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+
reports.push(setupProject(projectDir), await updateProject(projectDir));
67+
}
6268

6369
const failedReport = reports.find(report => Object.values(report).some(value => value.status === 'danger'));
6470
if (failedReport) {
@@ -69,12 +75,7 @@ export class ProjectService {
6975
await startStarter(projectDir);
7076
}
7177

72-
return {
73-
...createReport,
74-
...agentReport,
75-
...setupProjectReport,
76-
...updateReport
77-
};
78+
return Object.assign({}, ...reports);
7879
}
7980

8081
@tool({

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

Lines changed: 38 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,40 @@ describe('getDependencyInstallFailureMessage', () => {
198202
});
199203
});
200204

205+
describe('starter CDN URLs', () => {
206+
const versions = {
207+
'@nvidia-elements/core': '1.2.3',
208+
'@nvidia-elements/styles': '4.5.6',
209+
'@nvidia-elements/themes': '7.8.9'
210+
};
211+
212+
it('should create versioned esm URLs', () => {
213+
expect(
214+
createStarterCDNUrl('@nvidia-elements/core', versions['@nvidia-elements/core'], 'dist/bundles/index.js')
215+
).toBe('https://esm.sh/@nvidia-elements/core@1.2.3/dist/bundles/index.js');
216+
});
217+
218+
it('should stamp Elements CDN URLs with package versions', () => {
219+
const content = `
220+
<link rel="stylesheet" href="https://esm.sh/@nvidia-elements/styles/dist/bundles/index.css" />
221+
<link rel="stylesheet" href="https://esm.sh/@nvidia-elements/themes@0.0.1/dist/bundles/index.css" />
222+
<link rel="stylesheet" href="https://esm.sh/@nvidia-elements/themes/dist/fonts/inter.css" />
223+
<script type="module">
224+
import 'https://esm.sh/@nvidia-elements/core@0.0.1/dist/bundles/index.js';
225+
</script>
226+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>
227+
`;
228+
229+
const result = stampStarterCDNVersions(content, versions);
230+
231+
expect(result).toContain('https://esm.sh/@nvidia-elements/styles@4.5.6/dist/bundles/index.css');
232+
expect(result).toContain('https://esm.sh/@nvidia-elements/themes@7.8.9/dist/bundles/index.css');
233+
expect(result).toContain('https://esm.sh/@nvidia-elements/themes@7.8.9/dist/fonts/inter.css');
234+
expect(result).toContain('https://esm.sh/@nvidia-elements/core@1.2.3/dist/bundles/index.js');
235+
expect(result).toContain('https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js');
236+
});
237+
});
238+
201239
describe('getNPMClient', () => {
202240
it('should return the npm client', async () => {
203241
expect(await getNPMClient()).toBe('pnpm');

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

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { basename, dirname, join, parse, resolve } from 'node:path';
55
import { execFile, execFileSync } from 'node:child_process';
66
import { cwd } from 'node:process';
77

8-
import { writeFile } from 'fs/promises';
9-
import { existsSync, unlinkSync, writeFileSync, cpSync, createWriteStream, rmSync } from 'fs';
8+
import { readFile, writeFile } from 'fs/promises';
9+
import { existsSync, unlinkSync, writeFileSync, cpSync, createWriteStream, rmSync, readFileSync } from 'fs';
1010
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest';
1111
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config';
1212
import { createExportableManifest } from '@pnpm/exportable-manifest';
@@ -18,12 +18,33 @@ import type { Report } from '../internal/types.js';
1818
import { writeAllAgentConfigs } from './setup-agent.js';
1919

2020
const ELEMENTS_PAGES_BASE_URL = 'https://nvidia.github.io/elements';
21+
const ELEMENTS_CDN_BASE_URL = 'https://esm.sh';
22+
const REPO_WORKSPACE_DIR = '../../';
23+
24+
type StarterCDNPackageName = '@nvidia-elements/core' | '@nvidia-elements/styles' | '@nvidia-elements/themes';
25+
26+
const starterCDNPackagePaths: Record<StarterCDNPackageName, string> = {
27+
'@nvidia-elements/core': 'projects/core/package.json',
28+
'@nvidia-elements/styles': 'projects/styles/package.json',
29+
'@nvidia-elements/themes': 'projects/themes/package.json'
30+
};
31+
32+
const starterCDNAssets: { packageName: StarterCDNPackageName; filePath: string }[] = [
33+
{ packageName: '@nvidia-elements/core', filePath: 'dist/bundles/index.js' },
34+
{ packageName: '@nvidia-elements/styles', filePath: 'dist/bundles/index.css' },
35+
{ packageName: '@nvidia-elements/themes', filePath: 'dist/bundles/index.css' },
36+
{ packageName: '@nvidia-elements/themes', filePath: 'dist/fonts/inter.css' }
37+
];
38+
39+
const starterDirsWithStampedCDNVersions = new Set<string>(['go', 'go-htmx']);
2140

2241
export type Starter =
2342
| 'angular'
2443
| 'bundles'
2544
| 'eleventy'
2645
| 'go'
46+
| 'go-htmx'
47+
| 'hugo'
2748
| 'importmaps'
2849
| 'lit-library'
2950
| 'lit'
@@ -54,7 +75,13 @@ export const startersData = {
5475
},
5576
go: {
5677
zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go.zip`,
57-
cli: true
78+
cli: true,
79+
setupElementsDependencies: false
80+
},
81+
'go-htmx': {
82+
zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/go-htmx.zip`,
83+
cli: true,
84+
setupElementsDependencies: false
5885
},
5986
hugo: {
6087
zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/hugo.zip`,
@@ -113,7 +140,8 @@ export const startersData = {
113140
/* istanbul ignore next -- @preserve */
114141
export async function archiveStarter(projectDir: string, outDir: string) {
115142
const dist = join(outDir, projectDir);
116-
await copyProject(projectDir);
143+
await copyProject(projectDir, dist);
144+
await stampStarterCDNVersionFiles(projectDir, dist);
117145
writeAllAgentConfigs(dist);
118146
const packageJSON = await exportPackageFromWorkspace(projectDir);
119147
await writeFile(join(dist, 'package.json'), JSON.stringify(packageJSON, undefined, 2));
@@ -135,17 +163,64 @@ async function zipProject(outDir: string) {
135163
}
136164

137165
/* istanbul ignore next -- @preserve */
138-
function copyProject(projectDir: string) {
139-
const ignoreDirs = new Set(['dist', 'node_modules', '.wireit', '.eslintcache']);
140-
cpSync(projectDir, join('dist', projectDir), {
166+
function copyProject(projectDir: string, dist: string) {
167+
const ignoreDirs = new Set(['dist', 'node_modules', '.wireit', '.eslintcache', 'bin']);
168+
cpSync(projectDir, dist, {
141169
recursive: true,
142170
filter: src => !ignoreDirs.has(basename(src))
143171
});
144172
}
145173

174+
function escapeRegExp(value: string) {
175+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
176+
}
177+
178+
function getPackageVersion(repoRoot: string, packageName: StarterCDNPackageName): string {
179+
const packageJsonPath = join(repoRoot, starterCDNPackagePaths[packageName]);
180+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { version?: unknown };
181+
if (typeof packageJson.version !== 'string' || !packageJson.version) {
182+
throw new Error(`No version found for ${packageName} in ${packageJsonPath}`);
183+
}
184+
return packageJson.version;
185+
}
186+
187+
export function getStarterCDNPackageVersions(repoRoot: string): Record<StarterCDNPackageName, string> {
188+
return {
189+
'@nvidia-elements/core': getPackageVersion(repoRoot, '@nvidia-elements/core'),
190+
'@nvidia-elements/styles': getPackageVersion(repoRoot, '@nvidia-elements/styles'),
191+
'@nvidia-elements/themes': getPackageVersion(repoRoot, '@nvidia-elements/themes')
192+
};
193+
}
194+
195+
export function createStarterCDNUrl(packageName: StarterCDNPackageName, version: string, filePath: string) {
196+
return `${ELEMENTS_CDN_BASE_URL}/${packageName}@${version}/${filePath}`;
197+
}
198+
199+
export function stampStarterCDNVersions(content: string, versions: Record<StarterCDNPackageName, string>) {
200+
return starterCDNAssets.reduce((updatedContent, asset) => {
201+
const versionedUrl = createStarterCDNUrl(asset.packageName, versions[asset.packageName], asset.filePath);
202+
const urlPattern = new RegExp(
203+
`${escapeRegExp(ELEMENTS_CDN_BASE_URL)}/${escapeRegExp(asset.packageName)}(?:@[^/"']+)?/${escapeRegExp(asset.filePath)}`,
204+
'g'
205+
);
206+
return updatedContent.replace(urlPattern, versionedUrl);
207+
}, content);
208+
}
209+
210+
async function stampStarterCDNVersionFiles(projectDir: string, dist: string) {
211+
if (!starterDirsWithStampedCDNVersions.has(projectDir)) {
212+
return;
213+
}
214+
215+
const indexPath = join(dist, 'src/index.html');
216+
const repoRoot = resolve(REPO_WORKSPACE_DIR);
217+
const versions = getStarterCDNPackageVersions(repoRoot);
218+
const content = await readFile(indexPath, 'utf8');
219+
await writeFile(indexPath, stampStarterCDNVersions(content, versions));
220+
}
221+
146222
/* istanbul ignore next -- @preserve */
147223
async function exportPackageFromWorkspace(projectDir: string) {
148-
const REPO_WORKSPACE_DIR = '../../';
149224
const workspace = await readWorkspaceManifest(REPO_WORKSPACE_DIR);
150225
const catalogs = getCatalogsFromWorkspaceManifest(workspace);
151226
const manifest = await readProjectManifestOnly(projectDir);

projects/internals/tools/src/skills/about.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ Give a high level overview of the Elements Design System.
2121

2222
```bash
2323
# create a new project
24-
nve project.create # typescript, angular, react, lit, preact, solidjs, vue, nextjs, go
24+
nve project.create # angular, bundles, eleventy, go, go-htmx, hugo, nextjs, nuxt, react, solidjs, svelte, typescript, vue
2525
```
2626

2727
### Resources for Users
2828

2929
- [Documentation](https://NVIDIA.github.io/elements/)
30-
- [Gitlab Repo](https://github.com/NVIDIA/elements)
30+
- [GitHub Repo](https://github.com/NVIDIA/elements)
3131
- [Changelog](https://NVIDIA.github.io/elements/docs/changelog/)

projects/site/public/static/images/integrations/NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ These logos are **not** covered by this project's Apache 2.0 license.
1212
| cursor.svg | Anysphere Inc. |
1313
| eleventy.svg | Zach Leatherman |
1414
| go.svg | Google LLC |
15+
| htmx.svg | Big Sky Software |
1516
| hugo.svg | The Hugo Authors |
1617
| javascript.svg | Oracle Corporation (pending community usage) |
1718
| nextjs.svg | Vercel Inc. |

0 commit comments

Comments
 (0)