Skip to content
Draft
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
76 changes: 70 additions & 6 deletions packages/nx-extensions/src/executors/package-json/executor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as fs from 'fs';
import { createPackageJson } from '@nx/js';
import type { ExecutorContext, ProjectGraphDependency } from '@nx/devkit';
import type {
ExecutorContext,
FileData,
ProjectFileMap,
ProjectGraphDependency,
} from '@nx/devkit';
import {
serializeJson,
logger,
Expand All @@ -12,6 +17,8 @@ import {
HelperDependency,
readTsConfig,
} from '@nx/js';
import { readFileMapCache } from 'nx/src/project-graph/nx-deps-cache';
import { fileDataDepTarget } from 'nx/src/config/project-graph';
Comment on lines +20 to +21
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports reach into Nx internal source paths (nx/src/...), which are not part of the public API surface and can break on Nx upgrades or even patch releases. If possible, prefer a public @nx/devkit API (or encapsulate this behind a small compatibility layer with a well-defined fallback) so the executor remains stable across Nx versions.

Copilot uses AI. Check for mistakes.
import type { PackageJsonExecutorSchema } from './schema';

interface ExecutorEvent {
Expand Down Expand Up @@ -52,7 +59,12 @@ export default async function* packageJsonExecutor(
});
}

const monorepoDependencies = getMonorepoDependencies(context);
const sourceFileMap = getSourceOnlyFileMap();
const sourceDeps = getSourceDependencyTargets(
sourceFileMap,
context.projectName
);
const monorepoDependencies = getMonorepoDependencies(context, sourceDeps);

// Read optional dependencies from the original package.json
let originalOptionalDependencies: Record<string, string> | undefined;
Expand All @@ -75,7 +87,9 @@ export default async function* packageJsonExecutor(
context,
helperDependencies,
monorepoDependencies,
originalOptionalDependencies
originalOptionalDependencies,
sourceFileMap,
sourceDeps
);
if (built === false) {
return {
Expand Down Expand Up @@ -109,7 +123,9 @@ async function buildPackageJson(
context: ExecutorContext,
helperDependencies: ProjectGraphDependency[],
monorepoDependencies: MonorepoDependency[],
originalOptionalDependencies?: Record<string, string>
originalOptionalDependencies?: Record<string, string>,
sourceFileMap?: ProjectFileMap,
sourceDeps?: Set<string>
) {
const packageJson = createPackageJson(
context.projectName,
Expand All @@ -119,7 +135,8 @@ async function buildPackageJson(
root: context.root,
isProduction: true,
helperDependencies: helperDependencies.map((dep) => dep.target),
} as any
} as any,
sourceFileMap
);

let main = packageJson.main ?? event.outfile;
Expand All @@ -136,6 +153,17 @@ async function buildPackageJson(
packageJson.dependencies = {};
}

// Remove external dependencies that are not directly imported by source
// files. createPackageJson flattens transitive dependencies, but published
// libraries should only declare their direct dependencies.
if (sourceDeps) {
for (const name of Object.keys(packageJson.dependencies)) {
if (!sourceDeps.has(`npm:${name}`)) {
delete packageJson.dependencies[name];
}
}
}

for (const dep of monorepoDependencies) {
packageJson.dependencies[dep.name] = dep.version;
}
Expand Down Expand Up @@ -191,8 +219,41 @@ interface MonorepoDependency {
version: string;
}

function isSourceFile(filePath: string): boolean {
return !/\/test\//.test(filePath);
}

function getSourceOnlyFileMap(): ProjectFileMap {
const cache = readFileMapCache();
const fullFileMap = cache?.fileMap?.projectFileMap || {};
const filtered: ProjectFileMap = {};
for (const [project, files] of Object.entries(fullFileMap)) {
filtered[project] = (files as FileData[]).filter((f) =>
isSourceFile(f.file)
);
}
return filtered;
}
Comment on lines +226 to +236
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the Nx file map cache is missing/unavailable, fullFileMap becomes {}, and the executor still passes the resulting empty map into createPackageJson and uses it to compute sourceDeps. This can cause all dependencies (including legitimate runtime deps) to be dropped from the generated package.json. Consider returning undefined/null when the cache is unavailable and skipping both createPackageJson(..., sourceFileMap) and dependency filtering in that case (or falling back to the unfiltered createPackageJson behavior).

Copilot uses AI. Check for mistakes.

function getSourceDependencyTargets(
sourceFileMap: ProjectFileMap,
projectName?: string
): Set<string> {
const targets = new Set<string>();
if (projectName) {
const projectFiles = sourceFileMap[projectName] || [];
for (const fileData of projectFiles) {
for (const dep of fileData.deps || []) {
targets.add(fileDataDepTarget(dep));
}
}
}
return targets;
}

function getMonorepoDependencies(
context: ExecutorContext
context: ExecutorContext,
sourceDeps: Set<string>
): MonorepoDependency[] {
const monorepoDeps: MonorepoDependency[] = [];
for (const repoDep of context.projectGraph.dependencies[
Expand All @@ -207,6 +268,9 @@ function getMonorepoDependencies(
if (!(repoDep.target in context.projectGraph.nodes)) {
continue;
}
if (!sourceDeps.has(repoDep.target)) {
continue;
}
const targetSourceRoot =
context.projectGraph.nodes[repoDep.target].data.root;
const packageJsonPath = `${targetSourceRoot}/package.json`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './blob';
import '../lib/blob';

describe('File class', () => {
it('Should exist', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './custom-event';
import '../lib/custom-event';

describe('CustomEvent class', () => {
it('Should exist', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ProgressTracker } from './progress-tracker';
import type { ProgressTrackerEvent } from './progress-tracker';
import { ProgressTracker } from '../lib/progress-tracker';
import type { ProgressTrackerEvent } from '../lib/progress-tracker';

describe('Tracks total progress', () => {
it('A single ProgressTracker populated via fillSlowly (stopBeforeFinishing=true)', async () => {
Expand Down
118 changes: 1 addition & 117 deletions packages/php-wasm/scopes/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1 @@
/**
* Scopes are unique strings, like `my-site`, used to uniquely brand
* the outgoing HTTP traffic from each browser tab. This helps the
* main thread distinguish between the relevant and irrelevant
* messages received from the Service Worker.
*
* Scopes are included in the `PHPRequestHandler.absoluteUrl` as follows:
*
* An **unscoped** URL: http://localhost:8778/wp-login.php
* A **scoped** URL: http://localhost:8778/scope:my-site/wp-login.php
*
* For more information, see the README section on scopes.
*/

/**
* Checks if the given URL contains scope information.
*
* @example
* ```js
* isURLScoped(new URL('http://localhost/scope:my-site/index.php'));
* // true
*
* isURLScoped(new URL('http://localhost/index.php'));
* // false
* ```
*
* @param url The URL to check.
* @returns `true` if the URL contains scope information, `false` otherwise.
*/
export function isURLScoped(url: URL): boolean {
return url.pathname.startsWith(`/scope:`);
}

/**
* Returns the scope stored in the given URL.
*
* @example
* ```js
* getScopeFromURL(new URL('http://localhost/scope:my-site/index.php'));
* // '96253'
*
* getScopeFromURL(new URL('http://localhost/index.php'));
* // null
* ```
*
* @param url The URL.
* @returns The scope if the URL contains a scope, `null` otherwise.
*/
export function getURLScope(url: URL): string | null {
if (isURLScoped(url)) {
return url.pathname.split('/')[1].split(':')[1];
}
return null;
}

/**
* Returns a new URL with the requested scope information.
*
* @example
* ```js
* setURLScope(new URL('http://localhost/index.php'), 'my-site');
* // URL('http://localhost/scope:my-site/index.php')
*
* setURLScope(new URL('http://localhost/scope:my-site/index.php'), 'my-site');
* // URL('http://localhost/scope:my-site/index.php')
*
* setURLScope(new URL('http://localhost/index.php'), null);
* // URL('http://localhost/index.php')
* ```
*
* @param url The URL to scope.
* @param scope The scope value.
* @returns A new URL with the scope information in it.
*/
export function setURLScope(url: URL | string, scope: string | null): URL {
let newUrl = new URL(url);

if (isURLScoped(newUrl)) {
if (scope) {
const parts = newUrl.pathname.split('/');
parts[1] = `scope:${scope}`;
newUrl.pathname = parts.join('/');
} else {
newUrl = removeURLScope(newUrl);
}
} else if (scope) {
const suffix = newUrl.pathname === '/' ? '' : newUrl.pathname;
newUrl.pathname = `/scope:${scope}${suffix}`;
}

return newUrl;
}

/**
* Returns a new URL without any scope information.
*
* @example
* ```js
* removeURLScope(new URL('http://localhost/scope:my-site/index.php'));
* // URL('http://localhost/index.php')
*
* removeURLScope(new URL('http://localhost/index.php'));
* // URL('http://localhost/index.php')
* ```
*
* @param url The URL to remove scope information from.
* @returns A new URL without the scope information.
*/
export function removeURLScope(url: URL): URL {
if (!isURLScoped(url)) {
return url;
}
const newUrl = new URL(url);
const parts = newUrl.pathname.split('/');
newUrl.pathname = '/' + parts.slice(2).join('/');
return newUrl;
}
export * from './lib/scope';
117 changes: 117 additions & 0 deletions packages/php-wasm/scopes/src/lib/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Scopes are unique strings, like `my-site`, used to uniquely brand
* the outgoing HTTP traffic from each browser tab. This helps the
* main thread distinguish between the relevant and irrelevant
* messages received from the Service Worker.
*
* Scopes are included in the `PHPRequestHandler.absoluteUrl` as follows:
*
* An **unscoped** URL: http://localhost:8778/wp-login.php
* A **scoped** URL: http://localhost:8778/scope:my-site/wp-login.php
*
* For more information, see the README section on scopes.
*/

/**
* Checks if the given URL contains scope information.
*
* @example
* ```js
* isURLScoped(new URL('http://localhost/scope:my-site/index.php'));
* // true
*
* isURLScoped(new URL('http://localhost/index.php'));
* // false
* ```
*
* @param url The URL to check.
* @returns `true` if the URL contains scope information, `false` otherwise.
*/
export function isURLScoped(url: URL): boolean {
return url.pathname.startsWith(`/scope:`);
}

/**
* Returns the scope stored in the given URL.
*
* @example
* ```js
* getScopeFromURL(new URL('http://localhost/scope:my-site/index.php'));
* // '96253'
*
* getScopeFromURL(new URL('http://localhost/index.php'));
* // null
* ```
*
* @param url The URL.
* @returns The scope if the URL contains a scope, `null` otherwise.
*/
export function getURLScope(url: URL): string | null {
if (isURLScoped(url)) {
return url.pathname.split('/')[1].split(':')[1];
}
return null;
}

/**
* Returns a new URL with the requested scope information.
*
* @example
* ```js
* setURLScope(new URL('http://localhost/index.php'), 'my-site');
* // URL('http://localhost/scope:my-site/index.php')
*
* setURLScope(new URL('http://localhost/scope:my-site/index.php'), 'my-site');
* // URL('http://localhost/scope:my-site/index.php')
*
* setURLScope(new URL('http://localhost/index.php'), null);
* // URL('http://localhost/index.php')
* ```
*
* @param url The URL to scope.
* @param scope The scope value.
* @returns A new URL with the scope information in it.
*/
export function setURLScope(url: URL | string, scope: string | null): URL {
let newUrl = new URL(url);

if (isURLScoped(newUrl)) {
if (scope) {
const parts = newUrl.pathname.split('/');
parts[1] = `scope:${scope}`;
newUrl.pathname = parts.join('/');
} else {
newUrl = removeURLScope(newUrl);
}
} else if (scope) {
const suffix = newUrl.pathname === '/' ? '' : newUrl.pathname;
newUrl.pathname = `/scope:${scope}${suffix}`;
}

return newUrl;
}

/**
* Returns a new URL without any scope information.
*
* @example
* ```js
* removeURLScope(new URL('http://localhost/scope:my-site/index.php'));
* // URL('http://localhost/index.php')
*
* removeURLScope(new URL('http://localhost/index.php'));
* // URL('http://localhost/index.php')
* ```
*
* @param url The URL to remove scope information from.
* @returns A new URL without the scope information.
*/
export function removeURLScope(url: URL): URL {
if (!isURLScoped(url)) {
return url;
}
const newUrl = new URL(url);
const parts = newUrl.pathname.split('/');
newUrl.pathname = '/' + parts.slice(2).join('/');
return newUrl;
}
Loading
Loading