diff --git a/.github/workflows/web_build_and_test.yml b/.github/workflows/web_build_and_test.yml index d80edfd80..e988f6406 100644 --- a/.github/workflows/web_build_and_test.yml +++ b/.github/workflows/web_build_and_test.yml @@ -18,11 +18,11 @@ on: push: branches: [ main ] paths: - - 'renderers/web_core/**/*' + - 'renderers/web_core/**' - '.github/workflows/web_build_and_test.yml' pull_request: paths: - - 'renderers/web_core/**/*' + - 'renderers/web_core/**' - '.github/workflows/web_build_and_test.yml' jobs: @@ -49,6 +49,8 @@ jobs: working-directory: ./renderers/web_core run: npm run test lint: + runs-on: ubuntu-latest + steps: - uses: actions/checkout@v6 diff --git a/renderers/angular/package.json b/renderers/angular/package.json index bf145e82f..a92f5eb6e 100644 --- a/renderers/angular/package.json +++ b/renderers/angular/package.json @@ -23,7 +23,8 @@ "generate-examples": "node scripts/generate-examples.mjs", "build": "npm run generate-examples && ng build && node ../scripts/prepare-publish.mjs --source ./dist/package.json --skip-path-adjustment", "prepublishOnly": "node -e \"if(!process.cwd().endsWith('dist')) { console.error('Error: This package must be published from the dist/ directory. Run `npm run build` then `npm publish dist/`'); process.exit(1); }\"", - "publish:package": "npm run build && npm publish dist/ --access public", + "publish:package": "npm run build && npm publish dist/", + "publish:public": "npm run publish:package -- --access public", "demo": "npm run generate-examples && ng serve a2ui_explorer", "test": "npm run generate-examples && ng test --browsers=ChromeHeadless", "test:ci": "npm run generate-examples && ng test --watch=false --browsers=ChromeHeadless" diff --git a/renderers/docs/web_publishing.md b/renderers/docs/web_publishing.md index 00fb455af..3e1ed0c2d 100644 --- a/renderers/docs/web_publishing.md +++ b/renderers/docs/web_publishing.md @@ -1,11 +1,12 @@ # Publishing Guide for A2UI Web Packages -This guide is for project maintainers. It details the manual publishing process to the npm registry for all four web-related packages in this repository: +This guide is for project maintainers. It details the manual publishing process to the npm registry for all web-related packages in this repository: 1. `@a2ui/web_core` 2. `@a2ui/lit` 3. `@a2ui/angular` -4. `@a2ui/markdown-it` +4. `@a2ui/react` +5. `@a2ui/markdown-it` --- @@ -24,101 +25,67 @@ Ensure you have an NPM Access Token with rights to the `@a2ui` organization. --- -## 📦 1. Publishing `@a2ui/web_core` +## 📦 Publishing Packages -This package does not have internal `file:` dependencies, so it can be published directly from its root. +All `@a2ui` web packages follow a similar build and publish workflow. They must be published from their generated `dist/` folders to ensure correct paths and clean `package.json` files. + +If you attempt to run `npm publish` in their root directories, it will fail and throw an error to protect against publishing broken paths. ### Pre-flight Checks 1. Ensure your working tree is clean and you are on the correct branch (e.g., `main`). -2. Update the `version` in `renderers/web_core/package.json`. -3. Verify all tests pass: +2. Update the `version` in the package's `package.json`. +3. If publishing a renderer (Lit, Angular, React), ensure `@a2ui/web_core` is already published (or its version string is correctly updated) since these packages will read that version number. +4. Verify all tests pass: ```sh - cd renderers/web_core npm run test ``` ### Publish to NPM -Because this is a scoped package (`@a2ui/`), you have two options: - -**Option A: Publish as Private, then promote to Public (Requires Paid NPM Account)** -1. Publish (defaults to private): - ```sh - npm publish - ``` -2. Verify the package looks correct on the npm website. -3. Promote to public: - ```sh - npm access public @a2ui/web_core - ``` - -**Option B: Publish directly as Public (Free or Paid NPM Account)** -```sh -npm publish --access public -``` - -*Note: NPM automatically executes the `prepack` script (`npm run build`), compiles the TypeScript, and generates the `dist/` directory right before creating the tarball.* - -**What exactly gets published?** -Only the `dist/` directory, `src/` directory (for sourcemaps), `package.json`, `README.md`, and `LICENSE` are included in the published package. This is strictly controlled by the `"files"` array in `package.json`. Internal files like this publishing guide, tests, and configuration scripts are excluded. - -**What about the License?** -The package is automatically published under the `Apache-2.0` open-source license, as defined in `package.json`. - ---- -## 📦 2. Publishing `@a2ui/lit`, `@a2ui/angular`, and `@a2ui/markdown-it` +Because these are scoped packages (`@a2ui/`), they default to **private** publishing. -These packages depend on `@a2ui/web_core` via a local `file:../web_core` path for development. Therefore, **they must be published from their generated `dist/` folders.** We use specialized scripts to automatically rewrite their `package.json` with the correct `@a2ui/web_core` npm version before publishing. - -If you attempt to run `npm publish` in their root directories, it will fail and throw an error to protect against publishing broken paths. - -### Pre-flight Checks -1. Ensure `@a2ui/web_core` is already published (or its version string is correctly updated) since these packages will read that version number. -2. Update the `version` in the package you want to publish (e.g., `renderers/lit/package.json`). -3. Ensure all tests pass. - -### Publish to NPM -For each of these packages, simply run their automated publish script: - -**For Lit:** +#### Option A: Publish as Private (Default) +This is useful for testing or internal releases. ```sh -cd renderers/lit npm run publish:package ``` -**For Angular:** +#### Option B: Publish as Public +Use this for official releases. ```sh -cd renderers/angular -npm run publish:package +npm run publish:public ``` -**For Markdown-it:** -```sh -cd renderers/markdown/markdown-it -npm run publish:package -``` +*Note: `npm run publish:public` is a shortcut for `npm run publish:package -- --access public`.* -### How It Works (Explanations) +--- + +## 🔍 How It Works (Explanations) **What happens during `npm run publish:package`?** -Before publishing, the script runs the necessary `build` command which processes the code. For Lit and Markdown-it, `prepare-publish.mjs` runs, and for Angular, `postprocess-build.mjs` runs. These scripts: -1. Copy `package.json`, `README.md`, and `LICENSE` to the `dist/` folder. -2. Read the `version` from `@a2ui/web_core`. -3. Update the `file:` dependency in the `dist/package.json` to the actual core version (e.g., `^0.8.0`). -4. Adjust exports and paths to be relative to `dist/`. -5. Remove any build scripts (`prepublishOnly`, `scripts`) so they don't interfere with the publish process. +Before publishing, the script runs the necessary `build` command which processes the code. Then, a preparation script (usually `prepare-publish.mjs`) runs, which: +1. Copies `package.json`, `README.md`, and `LICENSE` to the `dist/` folder. +2. If it's a renderer, it reads the `version` from `@a2ui/web_core` and updates the `file:` dependency in the `dist/package.json` to the actual core version (e.g., `^0.9.0`). +3. Adjusts exports and paths (removing the `./dist/` prefix) so they are correct when consumed from the package root. +4. Removes any build scripts (`prepublishOnly`, `scripts`, `wireit`) so they don't interfere with the publish process. The `npm publish dist/` command then uploads only the contents of the `dist/` directory to the npm registry. +**What exactly gets published?** +Only the `dist/` directory, `src/` directory (for sourcemaps), `package.json`, `README.md`, and `LICENSE` are included in the published package. This is strictly controlled by the `"files"` array in the original `package.json`. + +**What about the License?** +The package is automatically published under the `Apache-2.0` open-source license, as defined in `package.json`. + --- ## 🔖 Post-Publish 1. Tag the release (replace with actual version): ```sh - git tag v0.8.0 + git tag v0.9.0 ``` 2. Push the tag: ```sh - git push origin v0.8.0 + git push origin v0.9.0 ``` 3. Create a GitHub Release mapping to the new tag. diff --git a/renderers/lit/package.json b/renderers/lit/package.json index 6894d5516..6542da5a2 100644 --- a/renderers/lit/package.json +++ b/renderers/lit/package.json @@ -48,7 +48,8 @@ "type": "module", "scripts": { "prepublishOnly": "node -e \"if(!process.cwd().endsWith('dist')) { console.error('Error: This package must be published from the dist/ directory. Run `npm run build` then `npm publish dist/`'); process.exit(1); }\"", - "publish:package": "npm run build && node ../scripts/prepare-publish.mjs && npm publish dist/ --access public", + "publish:package": "npm run build && node ../scripts/prepare-publish.mjs && npm publish dist/", + "publish:public": "npm run publish:package -- --access public", "build": "wireit", "build:tsc": "wireit", "dev": "npm run serve --watch", diff --git a/renderers/markdown/markdown-it/package.json b/renderers/markdown/markdown-it/package.json index e5e5888c8..8af912f40 100644 --- a/renderers/markdown/markdown-it/package.json +++ b/renderers/markdown/markdown-it/package.json @@ -37,7 +37,8 @@ "format": "prettier --ignore-path ../.gitignore --write .", "format:check": "prettier --ignore-path ../.gitignore --check .", "prepublishOnly": "node -e \"if(!process.cwd().endsWith('dist')) { console.error('Error: This package must be published from the dist/ directory. Run `npm run build` then `npm publish dist/`'); process.exit(1); }\"", - "publish:package": "npm run build && node prepare-publish.mjs && npm publish dist/ --access public", + "publish:package": "npm run build && node ../../scripts/prepare-publish.mjs && npm publish dist/", + "publish:public": "npm run publish:package -- --access public", "test": "wireit" }, "dependencies": { diff --git a/renderers/markdown/markdown-it/prepare-publish.mjs b/renderers/markdown/markdown-it/prepare-publish.mjs deleted file mode 100644 index 43f2ae2c1..000000000 --- a/renderers/markdown/markdown-it/prepare-publish.mjs +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs'; -import { join } from 'path'; - -// This script prepares the markdown-it package for publishing by: -// 1. Copying package.json to dist/ -// 2. Updating @a2ui/web_core dependency from 'file:...' to the actual version -// 3. Adjusting paths in package.json (main, types, exports) to be relative to dist/ - -const dirname = import.meta.dirname; -const corePkgPath = join(dirname, '../../web_core/package.json'); -const pkgPath = join(dirname, './package.json'); -const distDir = join(dirname, './dist'); - -if (!existsSync(distDir)) { - mkdirSync(distDir, { recursive: true }); -} - -// 1. Get Core Version -const corePkg = JSON.parse(readFileSync(corePkgPath, 'utf8')); -const coreVersion = corePkg.version; -if (!coreVersion) throw new Error('Cannot determine @a2ui/web_core version'); - -// 2. Read Package -const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); - -// 3. Update Dependency -if (pkg.peerDependencies && pkg.peerDependencies['@a2ui/web_core']) { - pkg.peerDependencies['@a2ui/web_core'] = coreVersion; -} else { - throw new Error('Error: @a2ui/web_core not found in peerDependencies. This is a mandatory dependency for publishing.'); -} -if (pkg.devDependencies && pkg.devDependencies['@a2ui/web_core']) { - // We can just remove devDependencies for the published package, or update it - pkg.devDependencies['@a2ui/web_core'] = coreVersion; -} - -// 4. Adjust Paths for Dist -pkg.main = adjustPath(pkg.main); -pkg.types = adjustPath(pkg.types); - -if (pkg.exports) { - for (const key in pkg.exports) { - const exp = pkg.exports[key]; - if (typeof exp === 'string') { - pkg.exports[key] = adjustPath(exp); - } else { - if (exp.types) exp.types = adjustPath(exp.types); - if (exp.default) exp.default = adjustPath(exp.default); - if (exp.import) exp.import = adjustPath(exp.import); - if (exp.require) exp.require = adjustPath(exp.require); - } - } -} - -// Remove files and wireit properties since we are publishing the dist folder directly -delete pkg.files; -delete pkg.wireit; -delete pkg.scripts; - -// 5. Write to dist/package.json -writeFileSync(join(distDir, 'package.json'), JSON.stringify(pkg, null, 2)); - -// 6. Copy README and LICENSE -// LICENSE is in the root directory for this package structure, or we can copy from parent -const licenseSrc = join(dirname, '../../../LICENSE'); -const readmeSrc = join(dirname, 'README.md'); - -if (existsSync(readmeSrc)) { - copyFileSync(readmeSrc, join(distDir, 'README.md')); -} -if (existsSync(licenseSrc)) { - copyFileSync(licenseSrc, join(distDir, 'LICENSE')); -} else { - console.warn("Could not find LICENSE at " + licenseSrc); -} - -console.log(`Prepared dist/package.json with @a2ui/web_core@${coreVersion}`); - -// Utility function to adjust the paths of the built files (dist/src/*) to (src/*) -function adjustPath(p) { - if (p && p.startsWith('./dist/')) { - return './' + p.substring(7); // Remove ./dist/ - } - return p; -} diff --git a/renderers/react/package.json b/renderers/react/package.json index 1d2771136..2b904e015 100644 --- a/renderers/react/package.json +++ b/renderers/react/package.json @@ -74,7 +74,8 @@ ], "scripts": { "prepublishOnly": "node -e \"if(!process.cwd().endsWith('dist')) { console.error('Error: This package must be published from the dist/ directory. Run `npm run build` then `npm publish dist/`'); process.exit(1); }\"", - "publish:package": "npm run build && node ../scripts/prepare-publish.mjs && npm publish dist/ --access public", + "publish:package": "npm run build && node ../scripts/prepare-publish.mjs && npm publish dist/", + "publish:public": "npm run publish:package -- --access public", "build": "tsup && node scripts/postbuild.mjs", "dev": "tsup --watch", "test": "vitest run", diff --git a/renderers/scripts/prepare-publish.mjs b/renderers/scripts/prepare-publish.mjs index 3f3b325c8..155fcbd51 100644 --- a/renderers/scripts/prepare-publish.mjs +++ b/renderers/scripts/prepare-publish.mjs @@ -17,6 +17,8 @@ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + // This script prepares a package for publishing. // Arguments: // --source : Path to the source package.json (defaults to ./package.json) @@ -35,9 +37,10 @@ for (let i = 0; i < args.length; i++) { } const packageDir = process.cwd(); +const scriptDir = fileURLToPath(new URL('.', import.meta.url)); const resolvedSourcePkg = resolve(packageDir, sourcePkgPath); const resolvedDistDir = resolve(packageDir, distDir); -const rootDir = resolve(packageDir, '../../'); +const rootDir = resolve(scriptDir, '../../'); if (!existsSync(resolvedDistDir)) { mkdirSync(resolvedDistDir, { recursive: true }); @@ -53,6 +56,9 @@ const pkg = JSON.parse(readFileSync(resolvedSourcePkg, 'utf8')); if (pkg.dependencies && pkg.dependencies['@a2ui/web_core']) { pkg.dependencies['@a2ui/web_core'] = '^' + coreVersion; } +if (pkg.peerDependencies && pkg.peerDependencies['@a2ui/web_core']) { + pkg.peerDependencies['@a2ui/web_core'] = '^' + coreVersion; +} // 3. Adjust paths if (!skipPathAdjustment) { diff --git a/renderers/web_core/package-lock.json b/renderers/web_core/package-lock.json index 17869ecb4..bc3794285 100644 --- a/renderers/web_core/package-lock.json +++ b/renderers/web_core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@a2ui/web_core", - "version": "0.8.8", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@a2ui/web_core", - "version": "0.8.8", + "version": "0.9.0", "license": "Apache-2.0", "dependencies": { "@preact/signals-core": "^1.13.0", diff --git a/renderers/web_core/package.json b/renderers/web_core/package.json index ba3cb73ea..f35ca438b 100644 --- a/renderers/web_core/package.json +++ b/renderers/web_core/package.json @@ -59,12 +59,13 @@ ], "scripts": { "prepublishOnly": "node -e \"if(!process.cwd().endsWith('dist')) { console.error('Error: This package must be published from the dist/ directory. Run `npm run build` then `npm publish dist/`'); process.exit(1); }\"", - "publish:package": "npm run build && node ../scripts/prepare-publish.mjs && npm publish dist/ --access public", + "publish:package": "npm run build && node ../scripts/prepare-publish.mjs && npm publish dist/", + "publish:public": "npm run publish:package -- --access public", "prepack": "npm run build", "build": "wireit", "build:tsc": "wireit", "test": "wireit", - "test:coverage": "c8 node --test \"dist/**/*.test.js\"", + "test:coverage": "c8 node --test dist/src/v0_8/*/*.test.js dist/src/v0_9/*/*.test.js dist/src/v0_9/*/*/*.test.js", "compile": "tsc", "lint": "gts lint", "fix": "gts fix" @@ -104,7 +105,7 @@ "clean": "if-file-deleted" }, "test": { - "command": "node --test dist", + "command": "node --test dist/src/v0_8/*/*.test.js dist/src/v0_9/*/*.test.js dist/src/v0_9/*/*/*.test.js", "dependencies": [ "build" ] diff --git a/renderers/web_core/src/v0_9/catalog/types.test.ts b/renderers/web_core/src/v0_9/catalog/types.test.ts index 1b6041f3c..d4044cc79 100644 --- a/renderers/web_core/src/v0_9/catalog/types.test.ts +++ b/renderers/web_core/src/v0_9/catalog/types.test.ts @@ -107,8 +107,8 @@ describe('InferredComponentApiSchemaType', () => { name: 'MockComp', schema: mockSchema, } satisfies ComponentApi; - // Appease typescript-eslint/no-unused-vars - type _ = typeof mockApi; + + assert.ok(mockApi); // Type-level equivalence assertion using z.infer type ExpectedType = z.infer; @@ -120,6 +120,7 @@ describe('InferredComponentApiSchemaType', () => { // This happens when `mockApi: ComponentApi`, but doesn't when // `mockApi {} satisfies ComponentApi`! const inferredIsAny: IsAny = false; + assert.strictEqual(inferredIsAny, false); // When types are not "any", check that they're the same by checking if they // extend each other. @@ -132,5 +133,6 @@ describe('InferredComponentApiSchemaType', () => { // typesMatchExact only accepts "true" if `TypesAreEquivalent` const typesMatchExact: TypesAreEquivalent = true; + assert.strictEqual(typesMatchExact, true); }); });