diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9801b479d..c327fee24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ This guide walks you through setting up the development environment and other im - [Export the new language bundle](#export-the-new-language-bundle) - [Add the export to the translations index](#add-the-export-to-the-translations-index) - [Test your translation](#test-your-translation) - - [Option 1: Using `npm` symlinks](#option-1-using-npm-symlinks) + - [Option 1: Using a `symlink`](#option-1-using-a-symlink) - [Option 2: Integrate into an existing sample](#option-2-integrate-into-an-existing-sample) - [Testing with AsgardeoProvider](#testing-with-asgardeoprovider) - [Update documentation](#update-documentation) @@ -244,25 +244,53 @@ pnpm build --filter @asgardeo/i18n To test your new language translation, you have two options: -##### Option 1: Using `npm` symlinks +##### Option 1: Using a symlink -Create a symlink to test your local changes without publishing: +From the workspace root, run the `symlink` script. It builds all packages, resolves `catalog:` and `workspace:*` references, and prints ready-to-paste override snippets: ```bash -# Navigate to the i18n package -cd packages/i18n +pnpm symlink +``` -# Create a global symlink -npm link +Copy the relevant snippet from the output into your test project's `package.json` and run `pnpm install` (or the equivalent for your package manager): -# Navigate to your test application -cd /path/to/your/test-app +###### pnpm -# Link the local i18n package -npm link @asgardeo/i18n +```json +{ + "pnpm": { + "overrides": { + "@asgardeo/i18n": "file:/path/to/javascript/packages/i18n" + } + } +} ``` -For more information about npm symlinks, see the [npm link documentation](https://docs.npmjs.com/cli/v10/commands/npm-link). +###### npm + +```json +{ + "overrides": { + "@asgardeo/i18n": "file:/path/to/javascript/packages/i18n" + } +} +``` + +###### Yarn (Berry) + +```json +{ + "resolutions": { + "@asgardeo/i18n": "file:/path/to/javascript/packages/i18n" + } +} +``` + +To restore the patched source files when you're done: + +```bash +git checkout packages/*/package.json +``` ##### Option 2: Integrate into an existing sample diff --git a/package.json b/package.json index 900966094..c5086d441 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "e2e:docker:down": "docker compose -f e2e/docker-compose.yml down -v", "e2e:docker:up:is": "docker compose -f e2e/docker-compose.yml up -d wso2is", "e2e:docker:up:thunder": "docker compose -f e2e/docker-compose.yml up -d thunder", - "e2e:install": "playwright install chromium" + "e2e:install": "playwright install chromium", + "symlink": "node scripts/symlink.js" }, "devDependencies": { "@changesets/changelog-github": "0.5.1", diff --git a/scripts/symlink.js b/scripts/symlink.js new file mode 100644 index 000000000..4a41dd33b --- /dev/null +++ b/scripts/symlink.js @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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. + */ + +/** + * Prepares local packages for symlinking into external projects. + * + * Problems this solves: + * - `catalog:` references in package.json are a pnpm workspace-only protocol. + * External consumers (even via `file:`) can't resolve them. + * - `workspace:*` references must become `file:` paths so the external project + * resolves inter-package dependencies to the local builds too. + * + * What it does: + * 1. Reads the `catalog:` entries from pnpm-workspace.yaml. + * 2. Builds all packages (pnpm build:packages). + * 3. Patches every `packages/<*>/package.json`, replacing: + * `"catalog:"` → the real version string from the catalog + * `"workspace:*"` → `"file:"` + * 4. Prints ready-to-paste override snippets for pnpm and npm. + * + * To restore the source files after you're done: + * git checkout packages/<*>/package.json + */ + +const fs = require('fs'); +const path = require('path'); +const {execSync} = require('child_process'); + +const ROOT = path.resolve(__dirname, '..'); + +// --------------------------------------------------------------------------- +// 1. Parse catalog from pnpm-workspace.yaml +// --------------------------------------------------------------------------- + +/** + * Minimal YAML parser for the flat `catalog:` section in pnpm-workspace.yaml. + * Handles both quoted and unquoted keys/values, and multi-word values. + */ +function parseCatalog() { + const yamlPath = path.join(ROOT, 'pnpm-workspace.yaml'); + const yaml = fs.readFileSync(yamlPath, 'utf-8'); + const catalog = {}; + let inCatalog = false; + + for (const raw of yaml.split('\n')) { + const line = raw.trimEnd(); + + if (/^catalog:\s*$/.test(line)) { + inCatalog = true; + continue; + } + + if (inCatalog) { + // A non-indented, non-empty line signals the end of the catalog block. + if (line.length > 0 && !/^\s/.test(line)) { + inCatalog = false; + continue; + } + + // Match ` 'key': value` or ` key: value` + const match = line.match(/^\s+['"]?([^'":\s][^'":]*?)['"]?\s*:\s*(.+)$/); + if (match) { + catalog[match[1].trim()] = match[2].trim().replace(/^['"]|['"]$/g, ''); + } + } + } + + return catalog; +} + +// --------------------------------------------------------------------------- +// 2. Discover all publishable packages (packages/* minus workspace exclusions) +// --------------------------------------------------------------------------- + +const EXCLUDED_PACKAGES = new Set(['nuxt']); // mirrors !packages/nuxt in pnpm-workspace.yaml + +function findPackages() { + const packagesDir = path.join(ROOT, 'packages'); + + return fs + .readdirSync(packagesDir, {withFileTypes: true}) + .filter(entry => entry.isDirectory() && !EXCLUDED_PACKAGES.has(entry.name)) + .map(entry => path.join(packagesDir, entry.name)) + .filter(pkgPath => fs.existsSync(path.join(pkgPath, 'package.json'))); +} + +// --------------------------------------------------------------------------- +// 3. Build packages +// --------------------------------------------------------------------------- + +function buildPackages() { + console.log('\nBuilding packages...\n'); + execSync('pnpm build:packages', {cwd: ROOT, stdio: 'inherit'}); + console.log('\nBuild complete.\n'); +} + +// --------------------------------------------------------------------------- +// 4. Patch package.json files – replace catalog: and workspace:* references +// --------------------------------------------------------------------------- + +const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']; + +function patchPackages(pkgPaths, catalog) { + // Build a name → absolute-path map for workspace packages. + const workspaceMap = {}; + for (const pkgPath of pkgPaths) { + const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8')); + if (pkgJson.name) workspaceMap[pkgJson.name] = pkgPath; + } + + for (const pkgPath of pkgPaths) { + const pkgJsonPath = path.join(pkgPath, 'package.json'); + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); + let modified = false; + + for (const field of DEP_FIELDS) { + if (!pkgJson[field]) continue; + + for (const [dep, version] of Object.entries(pkgJson[field])) { + // catalog: (default catalog) or catalog:name (named catalog – treated the same here) + if (typeof version === 'string' && version.startsWith('catalog:')) { + const resolved = catalog[dep]; + if (resolved) { + pkgJson[field][dep] = resolved; + modified = true; + } else { + console.warn(` [warn] No catalog entry for "${dep}" in ${pkgJson.name}`); + } + } + + if (version === 'workspace:*' || version === 'workspace:^' || version === 'workspace:~') { + const resolved = workspaceMap[dep]; + if (resolved) { + pkgJson[field][dep] = `file:${resolved}`; + modified = true; + } else { + console.warn(` [warn] Workspace package "${dep}" not found for ${pkgJson.name}`); + } + } + } + } + + if (modified) { + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n'); + console.log(` patched ${pkgJson.name}`); + } + } +} + +// --------------------------------------------------------------------------- +// 5. Print override snippets +// --------------------------------------------------------------------------- + +function printSnippets(pkgPaths) { + const overrides = {}; + for (const pkgPath of pkgPaths) { + const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8')); + if (pkgJson.name) overrides[pkgJson.name] = `file:${pkgPath}`; + } + + const divider = '─'.repeat(60); + + console.log(`\n${divider}`); + console.log(" pnpm — add to your project's package.json"); + console.log(divider); + console.log(JSON.stringify({pnpm: {overrides}}, null, 2)); + + console.log(`\n${divider}`); + console.log(" npm — add to your project's package.json"); + console.log(divider); + console.log(JSON.stringify({overrides}, null, 2)); + + console.log(`\n${divider}`); + console.log(" Yarn (Berry) — add to your project's package.json"); + console.log(divider); + console.log(JSON.stringify({resolutions: overrides}, null, 2)); + + console.log(`\n${divider}`); + console.log(' To restore source files when done:'); + console.log(' git checkout packages/*/package.json'); + console.log(divider + '\n'); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +console.log('symlink — preparing local packages for external linking\n'); + +const catalog = parseCatalog(); +console.log(`Catalog entries found: ${Object.keys(catalog).length}`); + +const pkgPaths = findPackages(); +console.log(`Packages found: ${pkgPaths.length} (${pkgPaths.map(p => path.basename(p)).join(', ')})`); + +buildPackages(); + +console.log('Patching package.json files...'); +patchPackages(pkgPaths, catalog); + +printSnippets(pkgPaths);