Skip to content
Open
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
36 changes: 34 additions & 2 deletions src/contact-summary/lib.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
var contactSummary = require('contact-summary.templated.js');
var contactSummaryEmitter = require('./contact-summary-emitter');
const contactSummary = require('contact-summary.templated.js');
const cardsExtensions = require('cht-cards-extensions-shim.js');
const contactSummaryEmitter = require('./contact-summary-emitter');

// Merge *.contact-summary.js extensions
let baseCards = contactSummary.cards || [];
let baseContext = contactSummary.context || {};
let baseFields = contactSummary.fields || [];

// Extension can export: { cards: [...], context: {...}, fields: [...] } or just an array of cards
function mergeExtension(extModule) {
if (!extModule) {
return;
}
if (Array.isArray(extModule)) {
baseCards = baseCards.concat(extModule);
return;
}
if (Array.isArray(extModule.cards)) {
baseCards = baseCards.concat(extModule.cards);
}
if (Array.isArray(extModule.fields)) {
baseFields = baseFields.concat(extModule.fields);
}
if (typeof extModule.context === 'object') {
Object.assign(baseContext, extModule.context);
}
}

cardsExtensions.forEach(mergeExtension);

contactSummary.cards = baseCards;
contactSummary.context = baseContext;
contactSummary.fields = baseFields;

module.exports = contactSummaryEmitter(contactSummary, contact, reports);
77 changes: 77 additions & 0 deletions src/lib/auto-include.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const path = require('node:path');
const fs = require('node:fs');

/**
* Check if a file matches the suffix pattern and is not excluded
* @param {string} file - File name to check
* @param {string} suffix - File suffix to match
* @param {string} exclude - File name to exclude
* @returns {boolean} True if file matches criteria
*/
const matchesSuffixPattern = (file, suffix, exclude) => {
return file.endsWith(suffix) && file !== exclude;
};

/**
* Check if a path points to a regular file
* @param {string} filePath - Path to check
* @returns {boolean} True if path is a regular file
*/
const isRegularFile = (filePath) => {
return fs.statSync(filePath).isFile();
};

/**
* Find all files matching a suffix pattern in a directory
* @param {string} projectDir - Project directory path
* @param {string} suffix - File suffix to match (e.g., '.tasks.js')
* @param {string} exclude - Main file to exclude (e.g., 'tasks.js')
* @returns {string[]} Array of matching file paths (sorted alphabetically)
*/
const findAutoIncludeFiles = (projectDir, suffix, exclude) => {
try {
const files = fs.readdirSync(projectDir);
const matchingFiles = files.filter(file => matchesSuffixPattern(file, suffix, exclude));
const sortedFiles = matchingFiles.sort();
const fullPaths = sortedFiles.map(file => path.join(projectDir, file));
return fullPaths.filter(isRegularFile);
} catch {
// Directory may not exist or may not be readable, which is expected
// when the project doesn't use auto-include files. Return empty array.
return [];
}
};

/**
* Find all *.tasks.js files (excluding tasks.js)
* @param {string} projectDir - Project directory path
* @returns {string[]} Array of matching file paths
*/
const findTasksExtensions = (projectDir) => {
return findAutoIncludeFiles(projectDir, '.tasks.js', 'tasks.js');
};

/**
* Find all *.targets.js files (excluding targets.js)
* @param {string} projectDir - Project directory path
* @returns {string[]} Array of matching file paths
*/
const findTargetsExtensions = (projectDir) => {
return findAutoIncludeFiles(projectDir, '.targets.js', 'targets.js');
};

/**
* Find all *.contact-summary.js files (excluding contact-summary.templated.js)
* @param {string} projectDir - Project directory path
* @returns {string[]} Array of matching file paths
*/
const findContactSummaryExtensions = (projectDir) => {
return findAutoIncludeFiles(projectDir, '.contact-summary.js', 'contact-summary.templated.js');
};

module.exports = {
findAutoIncludeFiles,
findTasksExtensions,
findTargetsExtensions,
findContactSummaryExtensions,
};
78 changes: 63 additions & 15 deletions src/lib/compilation/compile-contact-summary.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,84 @@
const path = require('path');
const nodeFs = require('node:fs');
const fs = require('../sync-fs');
const pack = require('./package-lib');
const os = require('node:os');
const { findContactSummaryExtensions } = require('../auto-include');
const { info } = require('../log');

module.exports = async (projectDir, options) => {
const freeformPath = `${projectDir}/contact-summary.js`;
const structuredPath = `${projectDir}/contact-summary.templated.js`;
/**
* Validate that exactly one contact-summary file exists
* @param {string} freeformPath - Path to contact-summary.js
* @param {string} structuredPath - Path to contact-summary.templated.js
* @throws {Error} If neither or both files exist
*/
const validateContactSummaryFiles = (freeformPath, structuredPath) => {
const freeformExists = fs.exists(freeformPath);
const structuredExists = fs.exists(structuredPath);

const freeformPathExists = fs.exists(freeformPath);
const structuredPathExists = fs.exists(structuredPath);

if (!freeformPathExists && !structuredPathExists) {
if (!freeformExists && !structuredExists) {
throw new Error(
`Could not find contact-summary javascript at either of ${freeformPath} or ${structuredPath}. `
+ 'Please create one xor other of these files.'
);
}
if (freeformPathExists && structuredPathExists) {
if (freeformExists && structuredExists) {
throw new Error(
`Found contact-summary javascript at both ${freeformPath} and ${structuredPath}. `
+ 'Only one of these files should exist.'
);
}

return { freeformExists, structuredExists };
};

/**
* Register card extension aliases for webpack
* @param {string[]} extensions - Array of extension file paths
* @returns {Object} Webpack aliases map
*/
const registerExtensionAliases = (extensions) => {
const aliases = {};
extensions.forEach((filePath, index) => {
aliases[`cht-cards-extension-${index}.js`] = filePath;
info(`Auto-including contact-summary: ${path.basename(filePath)}`);
});
return aliases;
};

/**
* Generate shim file with explicit requires for webpack
* @param {string[]} extensions - Array of extension file paths
* @returns {string} Path to generated shim file
*/
const generateExtensionsShim = (extensions) => {
const shimPath = path.join(os.tmpdir(), 'cht-cards-extensions-shim.js');
const requires = extensions.map((_, i) => `require('cht-cards-extension-${i}.js')`).join(',\n ');
const content = extensions.length > 0
? `module.exports = [\n ${requires}\n];`
: 'module.exports = [];';
nodeFs.writeFileSync(shimPath, content);
return shimPath;
};

module.exports = async (projectDir, options) => {
const freeformPath = `${projectDir}/contact-summary.js`;
const structuredPath = `${projectDir}/contact-summary.templated.js`;

const { freeformExists, structuredExists } = validateContactSummaryFiles(freeformPath, structuredPath);

const baseEslintPath = path.join(__dirname, '../../contact-summary/.eslintrc');
const pathToDeclarativeLib = path.join(__dirname, '../../contact-summary/lib.js');
const pathToPack = freeformPathExists ? freeformPath : pathToDeclarativeLib;

/*
WebApp expects the contact-summary to make a bare return
This isn't a direct output option for webpack, so add some boilerplate
*/
const pathToPack = freeformExists ? freeformPath : pathToDeclarativeLib;

// Find and register auto-include files (only for templated mode)
const cardExtensions = structuredExists ? findContactSummaryExtensions(projectDir) : [];
const extraAliases = registerExtensionAliases(cardExtensions);
extraAliases['cht-cards-extensions-shim.js'] = generateExtensionsShim(cardExtensions);

// WebApp expects the contact-summary to make a bare return
// This isn't a direct output option for webpack, so add some boilerplate
const packOptions = Object.assign({}, options, { libraryTarget: 'ContactSummary' });
const code = await pack(projectDir, pathToPack, baseEslintPath, packOptions);
const code = await pack(projectDir, pathToPack, { baseEslintPath, options: packOptions, extraAliases });
return `var ContactSummary = {}; ${code} return ContactSummary;`;
};
44 changes: 42 additions & 2 deletions src/lib/compilation/compile-tasks-and-targets.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const path = require('path');
const os = require('node:os');
const nodeFs = require('node:fs');

const fs = require('../sync-fs');
const pack = require('./package-lib');
const nools = require('../nools-utils');
const validateDeclarativeSchema = require('./validate-declarative-schema');
const { findTasksExtensions, findTargetsExtensions } = require('../auto-include');
const { info } = require('../log');

const DECLARATIVE_NOOLS_FILES = [ 'tasks.js', 'targets.js' ];

Expand Down Expand Up @@ -53,8 +57,44 @@ const compileDeclarativeFiles = async (projectDir, options) => {

const pathToDeclarativeLib = path.join(__dirname, '../../nools/lib.js');
const baseEslintPath = path.join(__dirname, '../../nools/.eslintrc');

return pack(projectDir, pathToDeclarativeLib, baseEslintPath, options);

// Find auto-include files
const tasksExtensions = findTasksExtensions(projectDir);
const targetsExtensions = findTargetsExtensions(projectDir);

// Build webpack aliases for extensions
const extraAliases = {};

tasksExtensions.forEach((filePath, index) => {
const aliasName = `cht-tasks-extension-${index}.js`;
extraAliases[aliasName] = filePath;
info(`Auto-including tasks: ${path.basename(filePath)}`);
});

targetsExtensions.forEach((filePath, index) => {
const aliasName = `cht-targets-extension-${index}.js`;
extraAliases[aliasName] = filePath;
info(`Auto-including targets: ${path.basename(filePath)}`);
});

// Generate shim that explicitly requires all extensions (webpack needs static requires)
const tasksShimPath = path.join(os.tmpdir(), 'cht-tasks-extensions-shim.js');
const tasksRequires = tasksExtensions.map((_, i) => `require('cht-tasks-extension-${i}.js')`).join(',\n ');
const tasksShimContent = tasksExtensions.length > 0
? `module.exports = [\n ${tasksRequires}\n].flat();`
: 'module.exports = [];';
nodeFs.writeFileSync(tasksShimPath, tasksShimContent);
extraAliases['cht-tasks-extensions-shim.js'] = tasksShimPath;

const targetsShimPath = path.join(os.tmpdir(), 'cht-targets-extensions-shim.js');
const targetsRequires = targetsExtensions.map((_, i) => `require('cht-targets-extension-${i}.js')`).join(',\n ');
const targetsShimContent = targetsExtensions.length > 0
? `module.exports = [\n ${targetsRequires}\n].flat();`
: 'module.exports = [];';
nodeFs.writeFileSync(targetsShimPath, targetsShimContent);
extraAliases['cht-targets-extensions-shim.js'] = targetsShimPath;

return pack(projectDir, pathToDeclarativeLib, { baseEslintPath, options, extraAliases });
};

module.exports = compileTasksAndTargets;
4 changes: 3 additions & 1 deletion src/lib/compilation/package-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const webpack = require('webpack');
const fsUtils = require('../sync-fs');
const { info, warn, error } = require('../log');

module.exports = (pathToProject, entry, baseEslintPath, options = {}) => {
module.exports = (pathToProject, entry, config = {}) => {
const { baseEslintPath, options = {}, extraAliases = {} } = config;
const baseEslintConfig = fsUtils.readJson(baseEslintPath);

const directoryContainingEntry = path.dirname(entry);
Expand Down Expand Up @@ -47,6 +48,7 @@ module.exports = (pathToProject, entry, baseEslintPath, options = {}) => {
'tasks.js': path.join(pathToProject, 'tasks.js'),
'targets.js': path.join(pathToProject, 'targets.js'),
'contact-summary.templated.js': path.join(pathToProject, 'contact-summary.templated.js'),
...extraAliases,
},
},
resolveLoader: {
Expand Down
18 changes: 12 additions & 6 deletions src/nools/lib.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/* global c, emit, Task, Target */

var tasks = require('tasks.js');
var targets = require('targets.js');
const tasks = require('tasks.js');
const targets = require('targets.js');
const tasksExtensions = require('cht-tasks-extensions-shim.js');
const targetsExtensions = require('cht-targets-extensions-shim.js');

var taskEmitter = require('./task-emitter');
var targetEmitter = require('./target-emitter');
const taskEmitter = require('./task-emitter');
const targetEmitter = require('./target-emitter');

targetEmitter(targets, c, Utils, Target, emit);
taskEmitter(tasks, c, Utils, Task, emit);
// Merge base tasks/targets with auto-included extensions
const allTasks = (Array.isArray(tasks) ? tasks : []).concat(tasksExtensions);
const allTargets = (Array.isArray(targets) ? targets : []).concat(targetsExtensions);

targetEmitter(allTargets, c, Utils, Target, emit);
taskEmitter(allTasks, c, Utils, Task, emit);

emit('_complete', { _id: true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
context: {
baseVar: 'from-base',
overrideMe: 'base-value',
},
fields: [
{
label: 'base.field',
value: 'base',
},
],
cards: [
{
label: 'base.card',
fields: [],
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
context: {
extensionVar: 'from-extension',
overrideMe: 'extension-value',
},
fields: [
{
label: 'extension.field',
value: 'extension',
},
],
cards: [
{
label: 'extension.card',
fields: [],
},
],
};
Loading