Skip to content

Commit bbf465f

Browse files
committed
feat: link local workspace packages for CI builds
Normalize internal @mapconductor/* dependency specs to "*" so npm links local git submodule workspaces instead of the unpublished registry. - add scripts/link-local-workspaces.mjs and `link:local` npm script - wire link step into deploy-basic-example workflow before install - switch CI from `npm ci` to `npm install` since specs are rewritten - pin example package deps to "*" for js-sdk and reactnative-basic - add generate-static.mjs for basic example static HTML generation - unignore the new scripts in .gitignore
1 parent 07d75ec commit bbf465f

7 files changed

Lines changed: 167 additions & 23 deletions

File tree

.github/workflows/deploy-basic-example.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,29 @@ jobs:
3939
with:
4040
node-version: 22
4141

42+
# The @mapconductor/* packages are not published to npm yet. They live in
43+
# this repo as git submodules (checked out above) and are wired up as npm
44+
# workspaces. Their package.json files pin each other to exact versions,
45+
# so once any package is version-bumped npm can no longer satisfy those
46+
# specs from the local workspaces and falls back to the registry, which
47+
# 404s. Normalize every internal spec to "*" so npm always links the
48+
# local submodule instead of downloading it.
49+
- name: Link local workspace packages
50+
run: node scripts/link-local-workspaces.mjs
51+
4252
- name: Cache node_modules
4353
id: node-modules-cache
4454
uses: actions/cache@v6
4555
with:
4656
path: node_modules
4757
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package.json') }}
4858

59+
# `npm install` (not `npm ci`): the link step above rewrites package.json
60+
# files at build time, so the committed lockfile is intentionally not kept
61+
# in exact sync and `npm ci` would fail the integrity check.
4962
- name: Install dependencies
5063
if: steps.node-modules-cache.outputs.cache-hit != 'true'
51-
run: npm ci
64+
run: npm install --no-audit --no-fund
5265

5366
- name: Build basic example
5467
run: npm run build

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ dist/*
5151
*.map
5252
*.mjs
5353
!examples/basic/server.mjs
54+
!examples/basic/scripts/generate-static.mjs
55+
!scripts/link-local-workspaces.mjs
5456
*.cjs
5557
!eslint.config.cjs
5658
dist/*.d.ts

examples/basic/package.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@
1111
"start": "vite preview --host 0.0.0.0 --port 4000"
1212
},
1313
"dependencies": {
14-
"@mapconductor/js-sdk-core": "0.1.0",
15-
"@mapconductor/js-sdk-react": "0.1.0",
16-
"@mapconductor/react-for-googlemaps": "0.1.0",
17-
"@mapconductor/react-for-leaflet": "0.1.1",
18-
"@mapconductor/react-for-mapbox": "0.1.0",
19-
"@mapconductor/react-for-maplibre": "0.1.0",
20-
"@mapconductor/react-for-openlayers": "0.1.1",
21-
"@mapconductor/react-for-arcgis": "0.1.0",
22-
"@mapconductor/react-for-cesium": "0.1.0",
23-
"@mapconductor/react-for-here": "0.1.0",
24-
"@mapconductor/react-geojson-layer": "0.1.0",
25-
"@mapconductor/react-heatmap": "0.1.0",
26-
"@mapconductor/react-icons": "0.1.0",
27-
"@mapconductor/react-marker-clustering": "0.1.0",
14+
"@mapconductor/js-sdk-core": "*",
15+
"@mapconductor/js-sdk-react": "*",
16+
"@mapconductor/react-for-googlemaps": "*",
17+
"@mapconductor/react-for-leaflet": "*",
18+
"@mapconductor/react-for-mapbox": "*",
19+
"@mapconductor/react-for-maplibre": "*",
20+
"@mapconductor/react-for-openlayers": "*",
21+
"@mapconductor/react-for-arcgis": "*",
22+
"@mapconductor/react-for-cesium": "*",
23+
"@mapconductor/react-for-here": "*",
24+
"@mapconductor/react-geojson-layer": "*",
25+
"@mapconductor/react-heatmap": "*",
26+
"@mapconductor/react-icons": "*",
27+
"@mapconductor/react-marker-clustering": "*",
2828
"@zip.js/zip.js": "^2.8.26",
2929
"prism-react-renderer": "^2.4.1",
3030
"react": "^19.2.7",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
2+
import { dirname, resolve } from 'node:path';
3+
import { pathToFileURL } from 'node:url';
4+
5+
const root = resolve(dirname(new URL(import.meta.url).pathname), '..');
6+
const outputRoot = resolve(root, 'dist');
7+
const template = await readFile(resolve(outputRoot, 'index.html'), 'utf8');
8+
const { render, getDocumentMetadata, getStaticPaths } = await import(
9+
pathToFileURL(resolve(outputRoot, 'server/entry-server.mjs')).href,
10+
);
11+
for (const pathname of getStaticPaths()) {
12+
const metadata = getDocumentMetadata(pathname);
13+
const html = renderDocument(pathname, metadata);
14+
const directory = resolve(outputRoot, pathname.slice(1));
15+
await mkdir(directory, { recursive: true });
16+
await writeFile(resolve(directory, 'index.html'), html);
17+
}
18+
19+
const defaultPath = '/maplibre/map/en';
20+
await writeFile(resolve(outputRoot, 'index.html'), renderDocument(defaultPath, getDocumentMetadata(defaultPath)));
21+
22+
function renderDocument(pathname, metadata) {
23+
return template
24+
.replace('<html lang="en">', `<html lang="${metadata.language}">`)
25+
.replace('<title>MapConductor React SDK - Basic Example</title>', `<title>${escapeHtml(metadata.title)}</title>`)
26+
.replace('</head>', `<meta name="description" content="${escapeHtml(metadata.description)}" /></head>`)
27+
.replace('<div id="root"></div>', `<div id="root">${render(pathname)}</div>`);
28+
}
29+
30+
function escapeHtml(value) {
31+
return value.replace(/[&<>"']/g, character => ({
32+
'&': '&amp;',
33+
'<': '&lt;',
34+
'>': '&gt;',
35+
'"': '&quot;',
36+
"'": '&#39;',
37+
}[character]));
38+
}

examples/reactnative-basic/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
"expo:ios": "expo run:ios"
1212
},
1313
"dependencies": {
14-
"@mapconductor/js-sdk-core": "0.1.0",
15-
"@mapconductor/js-sdk-react": "0.1.0",
16-
"@mapconductor/reactnative-for-googlemaps": "0.1.0",
17-
"@mapconductor/reactnative-for-maplibre": "0.1.1",
18-
"@mapconductor/reactnative-for-here": "0.1.0",
19-
"@mapconductor/react-geojson-layer": "0.1.0",
20-
"@mapconductor/react-heatmap": "0.1.0",
21-
"@mapconductor/react-marker-clustering": "0.1.0",
14+
"@mapconductor/js-sdk-core": "*",
15+
"@mapconductor/js-sdk-react": "*",
16+
"@mapconductor/reactnative-for-googlemaps": "*",
17+
"@mapconductor/reactnative-for-maplibre": "*",
18+
"@mapconductor/reactnative-for-here": "*",
19+
"@mapconductor/react-geojson-layer": "*",
20+
"@mapconductor/react-heatmap": "*",
21+
"@mapconductor/react-marker-clustering": "*",
2222
"@react-native-community/slider": "^5.2.0",
2323
"@react-native-picker/picker": "2.11.4",
2424
"expo": "~57.0.1",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"dev:packages": "(npm run dev --workspace @mapconductor/js-sdk-core -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/js-sdk-react -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-icons -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-for-googlemaps -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-for-maplibre -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-for-mapbox -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-for-leaflet -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-for-openlayers -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-for-cesium -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/reactnative-for-googlemaps -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/reactnative-for-maplibre -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/reactnative-for-arcgis -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/reactnative-for-here -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-geojson-layer -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-heatmap -- --tsconfig ../tsconfig.dev.json & npm run dev --workspace @mapconductor/react-marker-clustering -- --tsconfig ../tsconfig.dev.json & wait)",
3737
"dev:examples": "npm run dev --workspace @mapconductor/example-basic",
3838
"dev:rn:examples": "npm run expo:start --workspace @mapconductor/example-rn-basic -- --clear",
39+
"link:local": "node scripts/link-local-workspaces.mjs",
3940
"lint": "eslint .",
4041
"lint:fix": "eslint . --fix",
4142
"test": "npm run test --workspaces --if-present",

scripts/link-local-workspaces.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env node
2+
// Normalize internal @mapconductor/* dependency specs to "*" across every
3+
// workspace package.json so npm always links the local workspace (git
4+
// submodule) instead of falling back to the npm registry.
5+
//
6+
// The submodule packages pin each other to exact versions (e.g.
7+
// "@mapconductor/js-sdk-core": "0.1.0"). When a workspace has since been
8+
// bumped (e.g. to 0.1.1) that exact spec no longer matches the local
9+
// package, so npm tries to download it from registry.npmjs.org and fails
10+
// with a 404 because these packages are not published yet.
11+
//
12+
// Rewriting the internal specs to "*" makes npm satisfy them from the local
13+
// workspaces regardless of the exact version each package declares. We only
14+
// touch dependencies that point at packages living in this repo's
15+
// workspaces, so external deps are left untouched.
16+
//
17+
// Run this after `git submodule update --init` and before `npm install`.
18+
// It edits checked-out files in place; do not commit the submodule changes.
19+
20+
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
21+
import { dirname, join, resolve } from 'node:path';
22+
import { fileURLToPath } from 'node:url';
23+
24+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25+
26+
function readJson(path) {
27+
return JSON.parse(readFileSync(path, 'utf8'));
28+
}
29+
30+
// Resolve the workspaces entries (including simple "dir/*" globs) into
31+
// concrete directories that contain a package.json.
32+
const rootPkg = readJson(join(repoRoot, 'package.json'));
33+
const workspaceDirs = new Set();
34+
for (const pattern of rootPkg.workspaces ?? []) {
35+
const cleaned = pattern.replace(/\/$/, '');
36+
if (cleaned.endsWith('/*')) {
37+
const base = cleaned.slice(0, -2);
38+
const baseDir = join(repoRoot, base);
39+
if (!existsSync(baseDir)) continue;
40+
for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
41+
const dir = join(base, entry.name);
42+
if (entry.isDirectory() && existsSync(join(repoRoot, dir, 'package.json'))) {
43+
workspaceDirs.add(dir);
44+
}
45+
}
46+
} else if (cleaned.includes('*')) {
47+
throw new Error(`Unsupported workspaces glob pattern: ${pattern}`);
48+
} else if (existsSync(join(repoRoot, cleaned, 'package.json'))) {
49+
workspaceDirs.add(cleaned);
50+
}
51+
}
52+
53+
// Map of workspace package name -> directory, so we know which deps are local.
54+
const localNames = new Set();
55+
const pkgPaths = [];
56+
for (const dir of workspaceDirs) {
57+
const pkgPath = join(repoRoot, dir, 'package.json');
58+
const pkg = readJson(pkgPath);
59+
if (pkg.name) localNames.add(pkg.name);
60+
pkgPaths.push({ dir, pkgPath });
61+
}
62+
63+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
64+
65+
let changedFiles = 0;
66+
let changedSpecs = 0;
67+
for (const { dir, pkgPath } of pkgPaths) {
68+
const pkg = readJson(pkgPath);
69+
let touched = false;
70+
for (const field of DEP_FIELDS) {
71+
const deps = pkg[field];
72+
if (!deps) continue;
73+
for (const name of Object.keys(deps)) {
74+
if (localNames.has(name) && deps[name] !== '*') {
75+
deps[name] = '*';
76+
touched = true;
77+
changedSpecs++;
78+
}
79+
}
80+
}
81+
if (touched) {
82+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
83+
changedFiles++;
84+
console.log(` linked ${dir}/package.json`);
85+
}
86+
}
87+
88+
console.log(
89+
`link-local-workspaces: normalized ${changedSpecs} internal dep spec(s) across ${changedFiles} package(s) to "*".`,
90+
);

0 commit comments

Comments
 (0)