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
65 changes: 65 additions & 0 deletions .changeset/drupal-tokens-3x-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
'create-helix': patch
---

Wire `@helixui/tokens@3.x` into the Drupal scaffold (v0.9.3)

v0.9.2 deliberately left every Drupal preset pinned to `@helixui/tokens@^0.2.0`
with carve-outs in `doctor` + `upgrade` so they wouldn't push Drupal
scaffolds onto an unverified 3.x contract. Investigating that work for
v0.9.3 surfaced a deeper latent bug: the Drupal scaffold has **always**
declared `@helixui/tokens` as a dependency but **never** loaded its CSS,
so every `var(--hx-*, fallback)` reference in the generated theme silently
resolved to its inline fallback instead of the upstream brand token.

v0.9.3 closes that loop for **fresh** Drupal scaffolds:

- The generated `{theme}.libraries.yml` declares a dedicated `helix-tokens`
library that loads `css/vendor/helix-tokens.css` at `weight: -200`, and
`global` depends on it — tokens are in place before any theme CSS that
references them loads.
- The generated `css/style.css` `@import`s `vendor/helix-tokens.css` FIRST,
ahead of `helix-responsive.css` and `helix-overrides.css`. Cascade order
is: upstream tokens → responsive defaults → consumer overrides.
- **`css/vendor/helix-tokens.css` is vendored at SCAFFOLD time from a
BUILD-TIME-BUNDLED copy.** `scripts/add-shebang.mjs` runs at every
create-helix build and copies `@helixui/tokens/dist/tokens.css` into
`dist/assets/helix-tokens.css`. The published tarball ships that fixed
copy, and `scaffoldDrupalTheme` reads from it at scaffold time. This
makes the scaffold output deterministic per create-helix release — the
same create-helix version always emits the same bytes, independent of
how the installer's npm/pnpm/yarn resolves transitive deps. (A fallback
to runtime `require.resolve` of the package's exported CSS subpaths
covers vitest tests against `src/`, where the dist artifact doesn't
exist.) Scaffold-time vendoring also matters because Drupal theme users
typically don't run `npm install` inside the theme directory — the
documented setup is `cp -r theme/` + `drush theme:enable`, neither of
which fires a Node install.
- The scaffold also emits `scripts/copy-helix-tokens.mjs` and wires it to
the `package.json` postinstall hook. This is the REFRESH path: when a
developer does run `npm install` in the theme and gets a different
`@helixui/tokens` version, the vendored copy is kept in sync. The script
resolves `@helixui/tokens` via Node module resolution (`createRequire` +
`require.resolve` of the exported CSS subpaths — NOT `package.json`,
which `@helixui/tokens@3.x`'s exports map doesn't publish), so
hoisted/workspace installs work the same as flat ones.
- `src/presets/loader.ts` now imports `HELIX_TOKENS_VERSION` from
`helix-versions.ts` — Drupal joins every framework template on the
centralized pin (`^3.9.1`). `create-helix`'s own `@helixui/tokens`
dependency is bumped to the same range (and both lockfiles refreshed)
so the build-time bundling picks up the 3.9.1 bytes.

**`doctor` and `upgrade` exempt `@helixui/tokens` for ALL Drupal scaffolds
in this release.** The runtime token layer for any Drupal theme is
`css/vendor/helix-tokens.css`, not the declared range in `package.json`
or the contents of `node_modules/@helixui/tokens`. Bumping the pin alone
would advance the declaration while the theme keeps serving stale token
bytes (the documented `cp -r theme/` + `drush theme:enable` flow doesn't
run `npm install`, so the theme's postinstall script never fires to
refresh the vendored CSS). No honest `@helixui/tokens` upgrade exists for
an existing Drupal theme yet, so both checks skip it. The v0.9.4 follow-up
will make `runUpgrade` Drupal-theme-aware — refresh `css/vendor/helix-tokens.css`
from create-helix's bundled copy and, for pre-v0.9.3 themes, also inject
the wiring files (`scripts/copy-helix-tokens.mjs`, the `helix-tokens`
library entry, the `style.css` `@import`). At that point the skips can be
dropped entirely.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"fast-glob": "^3.3.3",
"fs-extra": "^11.3.0",
"picocolors": "^1.1.1",
"@helixui/tokens": "^3.3.1"
"@helixui/tokens": "^3.9.1"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 48 additions & 1 deletion scripts/add-shebang.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { readFileSync, writeFileSync } from 'fs';
import { readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const file = 'dist/index.js';
const shebang = '#!/usr/bin/env node\n';
Expand All @@ -7,3 +10,47 @@ const content = readFileSync(file, 'utf8');
if (!content.startsWith(shebang)) {
writeFileSync(file, shebang + content);
}

// ──────────────────────────────────────────────────────────────────────────
// Bundle @helixui/tokens/dist/tokens.css into dist/assets/helix-tokens.css.
//
// The Drupal scaffold vendors this CSS into its theme at scaffold time
// (see src/generators/drupal-theme.ts → readUpstreamHelixTokensCss). If the
// scaffold read directly from the installer's transitive node_modules at
// runtime, the same create-helix VERSION could emit different scaffold
// bytes depending on how npm/pnpm/yarn resolved @helixui/tokens at the
// installer's environment. Bundling here pins the bytes to whatever
// @helixui/tokens version is installed at create-helix's BUILD time, so
// the published tarball ships a fixed, deterministic copy.
//
// Resolved through the package's EXPORTED CSS subpaths — NOT package.json,
// which @helixui/tokens@3.x's exports map deliberately omits (resolving
// './package.json' throws ERR_PACKAGE_PATH_NOT_EXPORTED).
// ──────────────────────────────────────────────────────────────────────────
const scriptDir = dirname(fileURLToPath(import.meta.url));
const projectRoot = dirname(scriptDir);
const require = createRequire(import.meta.url);

let tokensCssSrc;
for (const subpath of ['@helixui/tokens/dist/tokens.css', '@helixui/tokens/tokens.css']) {
try {
tokensCssSrc = require.resolve(subpath);
break;
} catch {
/* try next subpath */
}
}

if (!tokensCssSrc) {
console.error(
'[build] @helixui/tokens not resolvable at build time — ' +
'check that @helixui/tokens is declared in dependencies.',
);
process.exit(1);
}

const assetsDest = join(projectRoot, 'dist', 'assets');
const tokensCssDest = join(assetsDest, 'helix-tokens.css');
mkdirSync(assetsDest, { recursive: true });
copyFileSync(tokensCssSrc, tokensCssDest);
console.log('[build] bundled', tokensCssSrc, '->', tokensCssDest);
36 changes: 30 additions & 6 deletions src/__tests__/doctor-extended.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,14 @@ describe('checkHelixLibrary / checkHelixTokens drift', () => {
expect(result.status).toBe('ok');
});

it('skips the @helixui/tokens drift check for a Drupal scaffold (0.x is intentional)', () => {
// Drupal presets pin @helixui/tokens to ^0.2.0 on purpose — v0.9.2 did
// not migrate the Drupal surface to 3.x. The drift check must NOT fail
// here and push the user toward an unverified upgrade.
it('skips the @helixui/tokens drift check for all Drupal scaffolds (deferred to v0.9.4)', () => {
// No honest signal exists yet for "is the runtime token layer current"
// on a Drupal theme — the runtime source is css/vendor/helix-tokens.css,
// which neither the declared range nor a node_modules lookup tells us
// about. v0.9.3 ships scaffold-time vendoring for FRESH scaffolds; the
// upgrade-time vendored-CSS refresh + pre-v0.9.3 theme-file migration
// is the v0.9.4 follow-up. Both pre- and post-v0.9.3 Drupal themes
// share this skip until that lands.
writeJson(path.join(tmp, 'package.json'), {
name: 'acme-theme',
dependencies: { '@helixui/drupal-starter': '^0.1.0', '@helixui/tokens': '^0.2.0' },
Expand All @@ -301,11 +305,31 @@ describe('checkHelixLibrary / checkHelixTokens drift', () => {
const result = checkHelixTokens(tmp);
expect(result.status).toBe('skip');
expect(result.message).toMatch(/Drupal scaffold/);
expect(result.message).toMatch(/v0\.9\.4/);
});

it('skips the @helixui/tokens drift check on a v0.9.3+ Drupal scaffold too', () => {
// The skip is blanket — having the v0.9.3 wiring marker doesn't make
// the declared-range signal meaningful (the runtime is the vendored
// CSS, not the range). The v0.9.4 follow-up adds the missing pieces.
writeJson(path.join(tmp, 'package.json'), {
name: 'acme-theme',
dependencies: { '@helixui/drupal-starter': '^0.1.0', '@helixui/tokens': '^3.9.1' },
});
fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
fs.writeFileSync(
path.join(tmp, 'scripts', 'copy-helix-tokens.mjs'),
'// v0.9.3+ wiring\n',
'utf8',
);
const result = checkHelixTokens(tmp);
expect(result.status).toBe('skip');
expect(result.message).toMatch(/Drupal scaffold/);
});

it('still runs the @helixui/library drift check for a Drupal scaffold (skip is tokens-only)', () => {
// The Drupal exemption is scoped to @helixui/tokens — if a Drupal theme
// somehow declares @helixui/library, that surface is NOT exempt.
// The Drupal exemption is narrowly scoped to @helixui/tokens — if a
// Drupal theme declares @helixui/library, that surface is NOT exempt.
writeJson(path.join(tmp, 'package.json'), {
name: 'acme-theme',
dependencies: { '@helixui/drupal-starter': '^0.1.0', '@helixui/library': '^1.0.0' },
Expand Down
Loading
Loading