Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,29 @@ jobs:
path: tests/e2e/playwright-report/
retention-days: 14

pack-smoke:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v6

- uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'

- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'

- run: npm ci
- run: npm run build
- run: dotnet restore
- run: dotnet pack -c Release -p:Version=0.0.0-ci --no-restore -o ./nupkgs

- name: Verify module .nupkgs contain static web assets
run: node scripts/verify-nupkg-static-assets.mjs ./nupkgs

docker:
runs-on: ubuntu-latest
needs: lint
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
cache: 'npm'

- run: npm ci
- run: npm run build
- run: dotnet restore
- run: dotnet pack -c Release -p:Version=${{ needs.release.outputs.version }} --no-restore -o ./nupkgs
- run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
Expand Down
37 changes: 36 additions & 1 deletion modules/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,20 @@
>
<Warning Text="node_modules not found. Frontend JavaScript will not be compiled. Run 'npm install' in the repository root." />
</Target>
<!--
Run BEFORE the targets that enumerate static web assets so the wwwroot/
contents Vite emits are visible to:
- Build (runtime path)
- ResolveStaticWebAssetsConfiguration / ResolveProjectStaticWebAssets (build-time manifest)
- GetCopyToOutputDirectoryItems (consumers via ProjectReference)
- GenerateNuspec (Pack — packaged staticwebassets/ tree)
Hooking only into Build leaves a hole for `dotnet pack` invoked with
no-build, and has historically produced .nupkg files with an empty
wwwroot in CI.
-->
<Target
Name="JsBuild"
BeforeTargets="Build"
BeforeTargets="Build;ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;GetCopyToOutputDirectoryItems;GenerateNuspec"
Condition="Exists('package.json') And (Exists('node_modules') Or Exists('$(RepoRoot)node_modules')) And '@(JsSourceFiles)' != ''"
Inputs="@(JsSourceFiles)"
Outputs="$(JsBuildStamp)"
Expand All @@ -34,4 +45,28 @@
<MakeDir Directories="$(IntermediateOutputPath)" />
<Touch Files="$(JsBuildStamp)" AlwaysCreate="true" />
</Target>
<!--
The StaticWebAssets SDK evaluates `Content Include="wwwroot\**"` during
MSBuild item evaluation, which runs BEFORE any target executes. On a
clean checkout wwwroot/ doesn't exist yet, so Content has no entries by
the time ResolveProjectStaticWebAssets enumerates @(Content). Even if
JsBuild later populates wwwroot/ via Vite, the static-web-asset chain
has already locked in an empty file list.

Re-include wwwroot files into @(Content) after JsBuild has run, before
ResolveProjectStaticWebAssets fires, so the SDK sees the actual built
output. Mirrors the wildcard from the SDK's StaticAssets.ProjectSystem
props (Content Include="wwwroot\**").
-->
<Target
Name="ReincludeWwwrootContent"
AfterTargets="JsBuild"
BeforeTargets="ResolveProjectStaticWebAssets"
Condition="Exists('$(MSBuildProjectDirectory)\wwwroot')"
>
<ItemGroup>
<_WwwrootFiles Include="wwwroot\**" Exclude="@(Content);$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Content Include="@(_WwwrootFiles)" ExcludeFromSingleFile="true" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Target>
</Project>
87 changes: 87 additions & 0 deletions scripts/verify-nupkg-static-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node
// Verify packed module .nupkgs contain their built static web assets.
//
// For every UI-shipping module (modules/*/src/*/ with a package.json next to
// the .csproj), assert the corresponding `{Id}.{version}.nupkg` contains
// `staticwebassets/{Id}.pages.js` (the SDK strips the `wwwroot/` prefix
// when packaging) and at least one .mjs code-split chunk — that combination
// matches the failure mode where 0.0.33 shipped only the .dll and content/.
//
// Usage: node scripts/verify-nupkg-static-assets.mjs <nupkg-dir>

import { execFileSync } from 'node:child_process';
import { existsSync, readdirSync, statSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..');

const nupkgDir = process.argv[2];
if (!nupkgDir) {
console.error('Usage: verify-nupkg-static-assets.mjs <nupkg-dir>');
process.exit(2);
}

const modulesDir = join(repoRoot, 'modules');
const moduleDirs = readdirSync(modulesDir)
.map((name) => join(modulesDir, name, 'src'))
.filter((p) => existsSync(p) && statSync(p).isDirectory())
.flatMap((srcDir) =>
readdirSync(srcDir)
.map((name) => join(srcDir, name))
.filter((p) => statSync(p).isDirectory()),
);

const uiModules = moduleDirs.filter((dir) => {
const csproj = readdirSync(dir).find((f) => f.endsWith('.csproj'));
return csproj && existsSync(join(dir, 'package.json')) && existsSync(join(dir, 'Pages', 'index.ts'));
});

if (uiModules.length === 0) {
console.error('No UI-shipping modules discovered under modules/.');
process.exit(2);
}

const nupkgs = readdirSync(nupkgDir).filter((f) => f.endsWith('.nupkg') && !f.endsWith('.symbols.nupkg'));

let failures = 0;

const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

for (const moduleDir of uiModules) {
const id = basename(moduleDir);
const matcher = new RegExp(`^${escapeRegExp(id)}\\.\\d+\\.\\d+\\.\\d+(?:[-+][\\w.+-]+)?\\.nupkg$`);
const nupkg = nupkgs.find((f) => matcher.test(f));
if (!nupkg) {
console.error(`FAIL ${id}: no matching .nupkg in ${nupkgDir}`);
failures++;
continue;
}

const listing = execFileSync('unzip', ['-Z1', join(nupkgDir, nupkg)], { encoding: 'utf8' })
.split('\n')
.filter(Boolean);

const pagesJs = `staticwebassets/${id}.pages.js`;
const hasPagesJs = listing.includes(pagesJs);
const hasMjsChunk = listing.some((p) => p.startsWith('staticwebassets/') && p.endsWith('.mjs'));
const hasBuildProps = listing.includes(`build/${id}.props`);

if (hasPagesJs && hasMjsChunk && hasBuildProps) {
console.log(`OK ${id}: ${nupkg}`);
} else {
console.error(`FAIL ${id}: ${nupkg}`);
if (!hasPagesJs) console.error(` missing ${pagesJs}`);
if (!hasMjsChunk) console.error(' no .mjs chunks under staticwebassets/');
if (!hasBuildProps) console.error(` missing build/${id}.props`);
failures++;
}
}

if (failures > 0) {
console.error(`\n${failures} module package(s) missing static web assets.`);
process.exit(1);
}

console.log(`\nVerified ${uiModules.length} module package(s).`);
Loading