Skip to content

Commit 6772574

Browse files
committed
Add symlink script for local package testing
1 parent ae01ffe commit 6772574

File tree

3 files changed

+258
-13
lines changed

3 files changed

+258
-13
lines changed

CONTRIBUTING.md

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ This guide walks you through setting up the development environment and other im
2222
- [Export the new language bundle](#export-the-new-language-bundle)
2323
- [Add the export to the translations index](#add-the-export-to-the-translations-index)
2424
- [Test your translation](#test-your-translation)
25-
- [Option 1: Using `npm` symlinks](#option-1-using-npm-symlinks)
25+
- [Option 1: Using a `symlink`](#option-1-using-a-symlink)
2626
- [Option 2: Integrate into an existing sample](#option-2-integrate-into-an-existing-sample)
2727
- [Testing with AsgardeoProvider](#testing-with-asgardeoprovider)
2828
- [Update documentation](#update-documentation)
@@ -244,25 +244,53 @@ pnpm build --filter @asgardeo/i18n
244244

245245
To test your new language translation, you have two options:
246246

247-
##### Option 1: Using `npm` symlinks
247+
##### Option 1: Using a symlink
248248

249-
Create a symlink to test your local changes without publishing:
249+
From the workspace root, run the `symlink` script. It builds all packages, resolves `catalog:` and `workspace:*` references, and prints ready-to-paste override snippets:
250250

251251
```bash
252-
# Navigate to the i18n package
253-
cd packages/i18n
252+
pnpm symlink
253+
```
254254

255-
# Create a global symlink
256-
npm link
255+
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):
257256

258-
# Navigate to your test application
259-
cd /path/to/your/test-app
257+
###### pnpm
260258

261-
# Link the local i18n package
262-
npm link @asgardeo/i18n
259+
```json
260+
{
261+
"pnpm": {
262+
"overrides": {
263+
"@asgardeo/i18n": "file:/path/to/javascript/packages/i18n"
264+
}
265+
}
266+
}
263267
```
264268

265-
For more information about npm symlinks, see the [npm link documentation](https://docs.npmjs.com/cli/v10/commands/npm-link).
269+
###### npm
270+
271+
```json
272+
{
273+
"overrides": {
274+
"@asgardeo/i18n": "file:/path/to/javascript/packages/i18n"
275+
}
276+
}
277+
```
278+
279+
###### Yarn (Berry)
280+
281+
```json
282+
{
283+
"resolutions": {
284+
"@asgardeo/i18n": "file:/path/to/javascript/packages/i18n"
285+
}
286+
}
287+
```
288+
289+
To restore the patched source files when you're done:
290+
291+
```bash
292+
git checkout packages/*/package.json
293+
```
266294

267295
##### Option 2: Integrate into an existing sample
268296

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"e2e:docker:down": "docker compose -f e2e/docker-compose.yml down -v",
3535
"e2e:docker:up:is": "docker compose -f e2e/docker-compose.yml up -d wso2is",
3636
"e2e:docker:up:thunder": "docker compose -f e2e/docker-compose.yml up -d thunder",
37-
"e2e:install": "playwright install chromium"
37+
"e2e:install": "playwright install chromium",
38+
"symlink": "node scripts/symlink.js"
3839
},
3940
"devDependencies": {
4041
"@changesets/changelog-github": "0.5.1",

scripts/symlink.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
/**
20+
* Prepares local packages for symlinking into external projects.
21+
*
22+
* Problems this solves:
23+
* - `catalog:` references in package.json are a pnpm workspace-only protocol.
24+
* External consumers (even via `file:`) can't resolve them.
25+
* - `workspace:*` references must become `file:` paths so the external project
26+
* resolves inter-package dependencies to the local builds too.
27+
*
28+
* What it does:
29+
* 1. Reads the `catalog:` entries from pnpm-workspace.yaml.
30+
* 2. Builds all packages (pnpm build:packages).
31+
* 3. Patches every `packages/<*>/package.json`, replacing:
32+
* `"catalog:"` → the real version string from the catalog
33+
* `"workspace:*"` → `"file:<absolute-path-to-package>"`
34+
* 4. Prints ready-to-paste override snippets for pnpm and npm.
35+
*
36+
* To restore the source files after you're done:
37+
* git checkout packages/<*>/package.json
38+
*/
39+
40+
const fs = require('fs');
41+
const path = require('path');
42+
const {execSync} = require('child_process');
43+
44+
const ROOT = path.resolve(__dirname, '..');
45+
46+
// ---------------------------------------------------------------------------
47+
// 1. Parse catalog from pnpm-workspace.yaml
48+
// ---------------------------------------------------------------------------
49+
50+
/**
51+
* Minimal YAML parser for the flat `catalog:` section in pnpm-workspace.yaml.
52+
* Handles both quoted and unquoted keys/values, and multi-word values.
53+
*/
54+
function parseCatalog() {
55+
const yamlPath = path.join(ROOT, 'pnpm-workspace.yaml');
56+
const yaml = fs.readFileSync(yamlPath, 'utf-8');
57+
const catalog = {};
58+
let inCatalog = false;
59+
60+
for (const raw of yaml.split('\n')) {
61+
const line = raw.trimEnd();
62+
63+
if (/^catalog:\s*$/.test(line)) {
64+
inCatalog = true;
65+
continue;
66+
}
67+
68+
if (inCatalog) {
69+
// A non-indented, non-empty line signals the end of the catalog block.
70+
if (line.length > 0 && !/^\s/.test(line)) {
71+
inCatalog = false;
72+
continue;
73+
}
74+
75+
// Match ` 'key': value` or ` key: value`
76+
const match = line.match(/^\s+['"]?([^'":\s][^'":]*?)['"]?\s*:\s*(.+)$/);
77+
if (match) {
78+
catalog[match[1].trim()] = match[2].trim().replace(/^['"]|['"]$/g, '');
79+
}
80+
}
81+
}
82+
83+
return catalog;
84+
}
85+
86+
// ---------------------------------------------------------------------------
87+
// 2. Discover all publishable packages (packages/* minus workspace exclusions)
88+
// ---------------------------------------------------------------------------
89+
90+
const EXCLUDED_PACKAGES = new Set(['nuxt']); // mirrors !packages/nuxt in pnpm-workspace.yaml
91+
92+
function findPackages() {
93+
const packagesDir = path.join(ROOT, 'packages');
94+
95+
return fs
96+
.readdirSync(packagesDir, {withFileTypes: true})
97+
.filter(entry => entry.isDirectory() && !EXCLUDED_PACKAGES.has(entry.name))
98+
.map(entry => path.join(packagesDir, entry.name))
99+
.filter(pkgPath => fs.existsSync(path.join(pkgPath, 'package.json')));
100+
}
101+
102+
// ---------------------------------------------------------------------------
103+
// 3. Build packages
104+
// ---------------------------------------------------------------------------
105+
106+
function buildPackages() {
107+
console.log('\nBuilding packages...\n');
108+
execSync('pnpm build:packages', {cwd: ROOT, stdio: 'inherit'});
109+
console.log('\nBuild complete.\n');
110+
}
111+
112+
// ---------------------------------------------------------------------------
113+
// 4. Patch package.json files – replace catalog: and workspace:* references
114+
// ---------------------------------------------------------------------------
115+
116+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
117+
118+
function patchPackages(pkgPaths, catalog) {
119+
// Build a name → absolute-path map for workspace packages.
120+
const workspaceMap = {};
121+
for (const pkgPath of pkgPaths) {
122+
const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8'));
123+
if (pkgJson.name) workspaceMap[pkgJson.name] = pkgPath;
124+
}
125+
126+
for (const pkgPath of pkgPaths) {
127+
const pkgJsonPath = path.join(pkgPath, 'package.json');
128+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
129+
let modified = false;
130+
131+
for (const field of DEP_FIELDS) {
132+
if (!pkgJson[field]) continue;
133+
134+
for (const [dep, version] of Object.entries(pkgJson[field])) {
135+
// catalog: (default catalog) or catalog:name (named catalog – treated the same here)
136+
if (typeof version === 'string' && version.startsWith('catalog:')) {
137+
const resolved = catalog[dep];
138+
if (resolved) {
139+
pkgJson[field][dep] = resolved;
140+
modified = true;
141+
} else {
142+
console.warn(` [warn] No catalog entry for "${dep}" in ${pkgJson.name}`);
143+
}
144+
}
145+
146+
if (version === 'workspace:*' || version === 'workspace:^' || version === 'workspace:~') {
147+
const resolved = workspaceMap[dep];
148+
if (resolved) {
149+
pkgJson[field][dep] = `file:${resolved}`;
150+
modified = true;
151+
} else {
152+
console.warn(` [warn] Workspace package "${dep}" not found for ${pkgJson.name}`);
153+
}
154+
}
155+
}
156+
}
157+
158+
if (modified) {
159+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n');
160+
console.log(` patched ${pkgJson.name}`);
161+
}
162+
}
163+
}
164+
165+
// ---------------------------------------------------------------------------
166+
// 5. Print override snippets
167+
// ---------------------------------------------------------------------------
168+
169+
function printSnippets(pkgPaths) {
170+
const overrides = {};
171+
for (const pkgPath of pkgPaths) {
172+
const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8'));
173+
if (pkgJson.name) overrides[pkgJson.name] = `file:${pkgPath}`;
174+
}
175+
176+
const divider = '─'.repeat(60);
177+
178+
console.log(`\n${divider}`);
179+
console.log(" pnpm — add to your project's package.json");
180+
console.log(divider);
181+
console.log(JSON.stringify({pnpm: {overrides}}, null, 2));
182+
183+
console.log(`\n${divider}`);
184+
console.log(" npm — add to your project's package.json");
185+
console.log(divider);
186+
console.log(JSON.stringify({overrides}, null, 2));
187+
188+
console.log(`\n${divider}`);
189+
console.log(" Yarn (Berry) — add to your project's package.json");
190+
console.log(divider);
191+
console.log(JSON.stringify({resolutions: overrides}, null, 2));
192+
193+
console.log(`\n${divider}`);
194+
console.log(' To restore source files when done:');
195+
console.log(' git checkout packages/*/package.json');
196+
console.log(divider + '\n');
197+
}
198+
199+
// ---------------------------------------------------------------------------
200+
// Main
201+
// ---------------------------------------------------------------------------
202+
203+
console.log('symlink — preparing local packages for external linking\n');
204+
205+
const catalog = parseCatalog();
206+
console.log(`Catalog entries found: ${Object.keys(catalog).length}`);
207+
208+
const pkgPaths = findPackages();
209+
console.log(`Packages found: ${pkgPaths.length} (${pkgPaths.map(p => path.basename(p)).join(', ')})`);
210+
211+
buildPackages();
212+
213+
console.log('Patching package.json files...');
214+
patchPackages(pkgPaths, catalog);
215+
216+
printSnippets(pkgPaths);

0 commit comments

Comments
 (0)