Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
107 changes: 105 additions & 2 deletions configs/tsdown/plugins/selective-bundle/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,100 @@ import {
realpathSync,
writeFileSync,
} from "node:fs";
import { resolve, sep } from "node:path";
import { dirname, resolve, sep } from "node:path";

import { PRIVATE_DEPS_LOOKUP } from "./constants.js";

const CATALOG_HEADER_REGEX = /^catalog:\s*$/;
const LEADING_WHITESPACE_REGEX = /^\s/;
const COMMENT_LINE_REGEX = /^\s*#/;
const CATALOG_ENTRY_REGEX = /^\s+(.+?)\s*:\s*(.+)$/;

/**
* Walks up the directory tree from `startDir` to find `pnpm-workspace.yaml`.
* @param {string} startDir
* @returns {string | null}
*/
function findWorkspaceYaml(startDir) {
let dir = startDir;
while (true) {
const candidate = resolve(dir, "pnpm-workspace.yaml");
if (existsSync(candidate)) {
return candidate;
}
const parent = dirname(dir);
if (parent === dir) {
return null;
}
dir = parent;
}
}

/**
* Parses the default `catalog:` block from a pnpm-workspace.yaml file.
* Does not require an external YAML library — only handles the simple
* flat key-value catalog structure used in this repo.
*
* @param {string} yamlPath
* @returns {Record<string, string>}
*/
function loadPnpmCatalog(yamlPath) {
const catalog = {};
const lines = readFileSync(yamlPath, "utf-8").split("\n");
let inCatalog = false;

for (const line of lines) {
if (CATALOG_HEADER_REGEX.test(line)) {
inCatalog = true;
continue;
}

if (inCatalog) {
// A non-empty, non-comment line without leading whitespace ends the block
if (
line.length > 0 &&
!LEADING_WHITESPACE_REGEX.test(line) &&
!COMMENT_LINE_REGEX.test(line)
) {
inCatalog = false;
continue;
}

// Parse " 'key': value" or " key: value"
const match = CATALOG_ENTRY_REGEX.exec(line.trimEnd());
if (match) {
const name = match[1].replace(/^['"]|['"]$/g, "").trim();
catalog[name] = match[2].trim();
}
}
}

return catalog;
}

/**
* Resolves `catalog:` protocol references in a deps object to their
* actual semver ranges from the workspace catalog.
*
* @param {Record<string, string> | undefined} deps
* @param {Record<string, string>} catalog
* @returns {Record<string, string> | undefined}
*/
function resolveCatalogRefs(deps, catalog) {
if (!deps) {
return deps;
}

return Object.fromEntries(
Object.entries(deps).map(([name, version]) => {
if (version === "catalog:" || version.startsWith("catalog:")) {
return [name, catalog[name] ?? version];
}
return [name, version];
}),
);
}

/**
* Gets the bare module name from an import source string.
* @param {string} source
Expand Down Expand Up @@ -110,10 +200,23 @@ export function buildEnrichedPackageJson(packageRoot, manifest) {
readFileSync(resolve(packageRoot, "package.json"), "utf-8"),
);

// Resolve catalog: protocol references so the packed tarball contains
// plain semver ranges that npm/yarn/bun can understand.
const workspaceYamlPath = findWorkspaceYaml(packageRoot);
const catalog = workspaceYamlPath ? loadPnpmCatalog(workspaceYamlPath) : {};
pkg.dependencies = resolveCatalogRefs(pkg.dependencies, catalog);
pkg.peerDependencies = resolveCatalogRefs(pkg.peerDependencies, catalog);
pkg.devDependencies = resolveCatalogRefs(pkg.devDependencies, catalog);

pkg.dependencies ??= {};

for (const [name, info] of Object.entries(manifest)) {
pkg.dependencies[name] ??= info.version;
// Resolve catalog: in transitive deps from private packages before merging
const version =
info.version === "catalog:" || info.version.startsWith("catalog:")
? (catalog[name] ?? info.version)
: info.version;
pkg.dependencies[name] ??= version;
}

pkg.dependencies = Object.fromEntries(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import type { CommerceAppConfigOutputModel } from "#config/schema/app";
import type { CommerceAppConfigDomain } from "#config/schema/domains";

type ActionConfig = {
requiresSchema?: boolean;
requiresEncryptionKey?: boolean;
requiresAuditFlag?: boolean;
};

export type TemplateAction = ActionConfig & {
Expand Down Expand Up @@ -62,7 +62,7 @@ function createActionDefinition(
actionName: string,
config: ActionConfig = {},
options: Omit<ActionDefinition, "function"> = {},
) {
): ActionDefinition {
const def: ActionDefinition = {
...options,

Expand All @@ -82,14 +82,24 @@ function createActionDefinition(
};
}

if (config.requiresAuditFlag) {
def.inputs = {
...def.inputs,
AIO_COMMERCE_CONFIG_AUDIT_ENABLED: "$AIO_COMMERCE_CONFIG_AUDIT_ENABLED",
};
}

return def;
}

/**
* Gets the runtime actions to be generated from the ext.config.yaml configuration.
* @param extConfig - The ext.config.yaml configuration.
*/
export function getRuntimeActions(extConfig: ExtConfig, dir: string) {
export function getRuntimeActions(
extConfig: ExtConfig,
dir: string,
): TemplateAction[] {
return Object.entries(
extConfig.runtimeManifest?.packages?.[PACKAGE_NAME]?.actions ?? {},
).map(
Expand All @@ -107,7 +117,7 @@ export function getRuntimeActions(extConfig: ExtConfig, dir: string) {
*/
export function buildAppManagementExtConfig(
appConfig: CommerceAppConfigOutputModel,
) {
): ExtConfig {
const features = getConfigDomains(appConfig);
const hasPasswordFieldsInSchema =
hasBusinessConfigSchema(appConfig) &&
Expand Down Expand Up @@ -171,13 +181,29 @@ export function buildAppManagementExtConfig(
}

/** Builds the ext.config.yaml configuration for the business configuration extension. */
export function buildBusinessConfigurationExtConfig() {
export function buildBusinessConfigurationExtConfig(): ExtConfig {
const actions = [
{
name: "config",
templateFile: "config.js.template",
requiresEncryptionKey: true,
},
{
name: "set-configuration",
templateFile: "set-configuration.js.template",
requiresEncryptionKey: true,
requiresAuditFlag: true,
},
{
name: "get-configuration-versions",
templateFile: "get-configuration-versions.js.template",
requiresAuditFlag: true,
},
{
name: "restore-configuration-version",
templateFile: "restore-configuration-version.js.template",
requiresAuditFlag: true,
},
{
name: "scope-tree",
templateFile: "scope-tree.js.template",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ async function generateActionFiles(
const outputFiles: string[] = [];
const templatesDir = join(__dirname, "generate/actions/templates");

if (extensionPointId === CONFIGURATION_EXTENSION_POINT_ID) {
const sharedTemplatePath = join(
templatesDir,
"business-configuration",
"audit-enabled.js.template",
);
const sharedOutputPath = join(outputDir, "audit-enabled.js");
const sharedTemplate = await readFile(sharedTemplatePath, "utf-8");
await writeFile(sharedOutputPath, sharedTemplate, "utf-8");
outputFiles.push(` ${relative(process.cwd(), sharedOutputPath)}`);
}

for (const action of actions) {
const templatePath = join(templatesDir, action.templateFile);
let template = await readFile(templatePath, "utf-8");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed 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 REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

export function parseAuditEnabled(value) {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
}
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed 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 REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

// This file has been auto-generated by `@adobe/aio-commerce-lib-config`
// Do not modify this file directly

import util from "node:util";

import {
byCode,
byCodeAndLevel,
byScopeId,
getConfigurationVersions,
setGlobalLibConfigOptions,
} from "@adobe/aio-commerce-lib-config";
import { parseAuditEnabled } from "./audit-enabled.js";
import {
badRequest,
internalServerError,
ok,
} from "@adobe/aio-commerce-sdk/core/responses";
import AioLogger from "@adobe/aio-lib-core-logging";

const inspect = (obj) => util.inspect(obj, { depth: null });

function parsePositiveInteger(value) {
if (value === undefined || value === null || value === "") {
return undefined;
}

const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) {
return null;
}
return parsed;
}

/**
* Get configuration version history.
* @param params - The input parameters.
* @returns The response object containing version history.
*/
export async function main(params) {
const logger = AioLogger("get-configuration-versions", {
level: params.LOG_LEVEL || "info",
});

try {
const auditEnabled = parseAuditEnabled(params.AIO_COMMERCE_CONFIG_AUDIT_ENABLED);
setGlobalLibConfigOptions({ auditEnabled });
if (!auditEnabled) {
return badRequest({
body: {
code: "AUDIT_DISABLED",
message: "Audit feature is disabled",
},
});
}

const id = params.id;
const code = params.code;
const level = params.level;
const limit = parsePositiveInteger(params.limit);
const offset = parsePositiveInteger(params.offset);

if (limit === null || offset === null) {
return badRequest({
body: {
code: "INVALID_PARAMS",
message: "limit and offset must be positive integers",
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

parsePositiveInteger currently accepts 0 (it only rejects values < 0), but the error message says "limit and offset must be positive integers". Either reject 0 or change the message to "non-negative integers" to match the validation.

Suggested change
message: "limit and offset must be positive integers",
message: "limit and offset must be non-negative integers",

Copilot uses AI. Check for mistakes.
},
});
}

if (!(id || code)) {
return badRequest({
body: {
code: "INVALID_PARAMS",
message: "Either id or code query param is required",
},
});
}

const selector = id
? byScopeId(id)
: level
? byCodeAndLevel(code, level)
: byCode(code);
const result = await getConfigurationVersions(selector, { limit, offset });

return ok({
body: result,
headers: {
"Cache-Control": "no-store",
},
});
} catch (error) {
logger.error(
`Something went wrong while retrieving configuration versions: ${inspect(error)}`,
);

if (error instanceof Error && error.message.includes("AUDIT_DISABLED")) {
return badRequest({
body: {
code: "AUDIT_DISABLED",
message: "Audit feature is disabled",
},
});
}

return internalServerError({
body: {
code: "INTERNAL_ERROR",
details: error instanceof Error ? error.message : "Unknown error",
message: "An internal server error occurred",
},
});
}
}
Loading
Loading