Skip to content

Commit

Permalink
Merge pull request #60 from scimmyjs/issue/45-typescript-definitions
Browse files Browse the repository at this point in the history
Fix generated TypeScript type definitions
  • Loading branch information
sleelin authored Dec 8, 2024
2 parents 06edabb + d368ad9 commit 75dc4f5
Show file tree
Hide file tree
Showing 32 changed files with 767 additions and 554 deletions.
484 changes: 235 additions & 249 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
"main": "./dist/scimmy.js",
"types": "./dist/scimmy.d.ts",
"exports": {
"import": "./dist/scimmy.js",
"require": "./dist/cjs/scimmy.cjs"
".": {
"import": "./dist/scimmy.js",
"require": "./dist/cjs/scimmy.cjs"
},
"./*": {
"import": "./dist/lib/*.js",
"require": "./dist/cjs/lib/*.js"
}
},
"scripts": {
"test": "node packager.js -t test",
Expand Down Expand Up @@ -60,11 +66,12 @@
"@types/node": "ts5.4",
"c8": "^10.1.2",
"chalk": "^5.3.0",
"classy-template": "^1.4.0",
"classy-template": "^1.5.2",
"jsdoc": "^4.0.2",
"minimist": "^1.2.8",
"mocha": "^10.4.0",
"rollup": "^4.17.2",
"ostensibly-typed": "^1.0.3",
"rollup": "^4.25.0",
"sinon": "^19.0.2",
"typescript": "^5.4.5"
}
Expand Down
107 changes: 49 additions & 58 deletions packager.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,21 @@ export class Packager {

/**
* Specified assets used throughout the packaging process
* @type {{entry, chunks, externals}}
* @typedef {Object} PackageAssets
* @prop {String} entry - module where the main export lives
* @prop {String[]} external - which modules should not be packaged
* @prop {String[]} chunks - which files to treat as defined chunks in the package
*/
static #assets = {
entry: "scimmy.js",
externals: [],
chunks: {
"lib/config": [`${Packager.paths.src}/lib/config.js`],
"lib/types": [`${Packager.paths.src}/lib/types.js`],
"lib/schemas": [`${Packager.paths.src}/lib/schemas.js`],
"lib/messages": [`${Packager.paths.src}/lib/messages.js`],
"lib/resources": [`${Packager.paths.src}/lib/resources.js`],
}
entry: "scimmy",
external: [],
chunks: [
"lib/config",
"lib/types",
"lib/schemas",
"lib/messages",
"lib/resources"
]
};

/**
Expand Down Expand Up @@ -77,7 +80,7 @@ export class Packager {
// Notify action failure (should only come when executing action)
if (!!pre) process.stdout.write(`${chalk.red("failed!")}\r\n`);
if (ex instanceof Function) ex();
else console.log(`${chalk.yellow("Reason: ")}${chalk.grey(ex.message)}\r\n`);
else console.log(`${chalk.yellow("Reason: ")}${chalk.grey(ex.message)}\r\n${ex.stack}`);
if (!!failure) console.log(chalk.red(failure));
process.exitCode = 1;
process.exit();
Expand Down Expand Up @@ -115,7 +118,7 @@ export class Packager {
pre: `Cleaning target build directory ${chalk.blue(dest)}: `,
action: async () => await Packager.clean(dest)
}]);

await step("Running Prebuild Tests", [{
pre: verbose ? false : "Running prebuild tests: ",
post: true,
Expand All @@ -132,15 +135,12 @@ export class Packager {
}
}]);

await step("Preparing TypeScript definitions", [{
pre: `Writing definitions to ${chalk.blue(dest)}/${chalk.magenta(Packager.#assets.entry.replace(".js", ".d.ts"))}: `,
post: "Generated type definitions for the following files:",
await step("Preparing TypeScript declarations", [{
pre: `Writing declarations to ${chalk.blue(dest)}/${chalk.magenta(`${Packager.#assets.entry}.d.ts`)}: `,
post: "Generated type definitions from the following files:",
action: async () => {
let bundles = await Packager.typedefs(
`${src}/${Packager.#assets.entry}`, `${dest}/${Packager.#assets.entry.replace(".js", ".d.ts")}`,
{allowJs: true, declaration: true, emitDeclarationOnly: true});

return bundles.map(file => file.replace(src, chalk.grey(src)));
const sources = await Packager.declare(src, dest, Packager.#assets);
return sources.map(file => file.replace(src, chalk.grey(src)));
}
}]);
}
Expand Down Expand Up @@ -182,39 +182,36 @@ export class Packager {
* Use RollupJS to bundle sources into defined packages
* @param {String} src - the source directory to read assets from
* @param {String} dest - the destination directory to write bundles to
* @param {Object} assets - entry-point and chunk files to pass to RollupJS
* @param {String} assets.entry - entry point for RollupJS
* @param {String[]} assets.externals - imports that are used but not local for RollupJS
* @param {Object} assets.chunks - chunk file definitions for RollupJS
* @param {PackageAssets} assets - entry-point and chunk files to pass to RollupJS
* @returns {Promise<String[]>} names of files generated by RollupJS
*/
static async rollup(src, dest, assets) {
const rollup = await import("rollup");
const {entry: input, chunks, externals} = assets;
const {entry, chunks, external} = assets;
const fileNameConfig = {esm: "[name].js", cjs: "[name].cjs"};
const input = {[entry]: path.join(src, `${entry}.js`), ...chunks.reduce((chunks, chunk) => Object.assign(chunks, {[chunk]: path.join(src, `${chunk}.js`)}), {})};
const manualChunks = chunks.reduce((chunks, chunk) => Object.assign(chunks, {[chunk]: [path.join(src, `${chunk}.js`)]}), {});
const output = [];
const config = {
exports: "auto",
manualChunks: chunks,
exports: "named", interop: "auto",
minifyInternalExports: false,
hoistTransitiveImports: false,
generatedCode: {
manualChunks, generatedCode: {
constBindings: true
}
};

// Prepare RollupJS bundle with supplied entry point
let bundle = await rollup.rollup({
input: path.join(src, input), external: externals,
onwarn: (warning, warn) => (warning.code !== "CIRCULAR_DEPENDENCY" ? warn(warning) : false)
// Prepare RollupJS bundle with supplied entry points
const bundle = await rollup.rollup({
external, input, onwarn: (warning, warn) =>
(warning.code !== "CIRCULAR_DEPENDENCY" ? warn(warning) : false)
});

// Construct the bundles with specified chunks in specified formats and write to destination
for (let format of ["esm", "cjs"]) {
let {output: results} = await bundle.write({
...config, format: format, dir: (format === "esm" ? dest : `${dest}/${format}`),
entryFileNames: fileNameConfig[format],
chunkFileNames: fileNameConfig[format]
const {output: results} = await bundle.write({
...config, format, dir: (format === "esm" ? dest : `${dest}/${format}`),
entryFileNames: fileNameConfig[format], chunkFileNames: fileNameConfig[format]
});

output.push(...results.map(file => (format === "esm" ? file.fileName : `${format}/${file.fileName}`)));
Expand All @@ -224,34 +221,28 @@ export class Packager {
}

/**
* Use TypeScript Compiler API to generate type definitions
* Use OstensiblyTyped and RollupJS to generate type definitions
* @param {String} src - the source directory to read assets from
* @param {String} dest - the destination file or directory to write compiled output to
* @param {Object} config - options to pass through to the TypeScript Compiler
* @param {String} dest - the destination file or directory to write generated output to
* @param {PackageAssets} assets - entry-point and chunk files to pass to RollupJS
* @returns {Promise<String[]>} names of files with generated types
*/
static async typedefs(src, dest, config) {
// Prepare a TypeScript Compiler Program for compilation
const {default: ts} = await import("typescript");
const program = ts.createProgram(Array.isArray(src) ? src : [src], {
// If destination is a TypeScript or JavaScript file, assume all sources are targeting a single file
...config, ...(dest.endsWith(".ts") || dest.endsWith(".js") ? {outFile: dest} : {outDir: dest})
});
static async declare(src, dest, assets) {
const rollup = await import("rollup");
const {generateDeclarations, filterGeneratedBundle} = await import("ostensibly-typed/plugin-rollup");
const {entry, external} = assets;

// Run the compiler instance
program.emit();
// Prepare RollupJS with OstensiblyTyped plugin and supplied entry point
const bundle = await rollup.rollup({
external, input: path.join(src, `${entry}.js`),
plugins: [generateDeclarations({moduleName: entry, defaultExport: "SCIMMY"})],
onwarn: (warning, warn) => (warning.code !== "CIRCULAR_DEPENDENCY" ? warn(warning) : false)
});

// Go through and get the results of which source files were read by the compiler
let output = [];
for (let sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
// Make sure the source file wasn't a TypeScript Library, then add the relative path to results
let fileName = sourceFile.fileName.replace(`${cwd}${path.sep}`, "./");
output.push(fileName.startsWith("./") ? fileName : `./${fileName}`);
}
}
// Construct the bundles with specified chunks in specified formats and write to destination
const {output: [{originalFileNames}]} = await bundle.write({dir: dest, format: "esm", plugins: [filterGeneratedBundle({emitDeclarationOnly: true})]});

return output;
return originalFileNames.map((fileName) => `./${path.relative(cwd, fileName)}`);
}
}

Expand Down
37 changes: 23 additions & 14 deletions src/lib/config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Schemas from "./schemas.js";
import {ServiceProviderConfig} from "./schemas.js";

// Define handler traps for returned proxied configuration object
const catchAll = () => {throw new TypeError("SCIM Configuration can only be changed via the 'set' method")};
const handleTraps = {set: catchAll, deleteProperty: catchAll, defineProperty: catchAll};

/**
* SCIMMY Service Provider Configuration Class
* @module scimmy/config
* @namespace SCIMMY.Config
* @description
* SCIMMY provides a singleton class, `SCIMMY.Config`, that acts as a central store for a SCIM Service Provider's configuration.
Expand Down Expand Up @@ -127,21 +128,29 @@ export default class Config {
}), {}), handleTraps);
}

/**
* Set multiple SCIM service provider configuration property values
* @overload
* @param {Object} config - the new configuration to apply to the service provider config instance
* @returns {Object} the updated configuration instance
*/
/**
* Set specific SCIM service provider configuration property by name
* @overload
* @param {String} name - the name of the configuration property to set
* @param {Object|Boolean} value - the new value of the configuration property to set
* @returns {typeof SCIMMY.Config} the config container class for chaining
*/
/**
* Set SCIM service provider configuration
* @param {Array<Object|String>} args - the configuration key name or value to apply
* @param {Object} args - the new configuration to apply to the service provider config instance
* @param {String} args - the name of the configuration property to set
* @param {Object|Boolean} args - the new value of the configuration property to set
* @param {Object|String} name - the configuration key name or value to apply
* @param {Object|String|Boolean} [config=name] - the new configuration to apply to the service provider config instance
* @returns {Object|typeof SCIMMY.Config} the updated configuration instance, or the config container class for chaining
*/
static set(...args) {
// Dereference name and config from supplied parameters
let [name, config = args[0]] = args;

static set(name, config = name) {
// If property name supplied, call again with object
if (typeof name === "string") {
Config.set({[name]: args[1]});
Config.set({[name]: config});

return Config;
}
Expand All @@ -163,12 +172,12 @@ export default class Config {
throw new TypeError("SCIM configuration: attribute 'documentationUri' expected value type 'string'");

// Assign documentationUri string
if (!!value) Config.#config.documentationUri = Schemas.ServiceProviderConfig.definition.attribute(key).coerce(value);
if (!!value) Config.#config.documentationUri = ServiceProviderConfig.definition.attribute(key).coerce(value);
else Config.#config.documentationUri = undefined;
} else if (Array.isArray(target)) {
// Target is multi-valued (authenticationSchemes), add coerced values to config, or reset if empty
if (!value || (Array.isArray(value) && value.length === 0)) target.splice(0);
else target.push(...Schemas.ServiceProviderConfig.definition.attribute(key).coerce(Array.isArray(value) ? value : [value]));
else target.push(...ServiceProviderConfig.definition.attribute(key).coerce(Array.isArray(value) ? value : [value]));
} else {
// Strings are not valid shorthand config values
if (typeof value === "string")
Expand All @@ -194,10 +203,10 @@ export default class Config {
else if (value === Object(value)) {
try {
// Make sure all object keys correspond to valid config attributes
for (let name of Object.keys(value)) Schemas.ServiceProviderConfig.definition.attribute(`${key}.${name}`);
for (let name of Object.keys(value)) ServiceProviderConfig.definition.attribute(`${key}.${name}`);

// Coerce the value and assign it to the config property
Object.assign(target, Schemas.ServiceProviderConfig.definition.attribute(key)
Object.assign(target, ServiceProviderConfig.definition.attribute(key)
.coerce({...target, supported: true, ...value}));
} catch (ex) {
// Rethrow exceptions after giving them better context
Expand Down
13 changes: 11 additions & 2 deletions src/lib/messages.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import {ErrorMessage} from "./messages/error.js";
import {ErrorResponse} from "./messages/error.js";
import {ListResponse} from "./messages/listresponse.js";
import {PatchOp} from "./messages/patchop.js";
import {BulkRequest} from "./messages/bulkrequest.js";
import {BulkResponse} from "./messages/bulkresponse.js";
import {SearchRequest} from "./messages/searchrequest.js";

// Export classes for direct consumption
export {ErrorResponse, ListResponse, PatchOp, BulkRequest, BulkResponse, SearchRequest};

/**
* SCIMMY Messages Container Class
* @module scimmy/messages
* @namespace SCIMMY.Messages
* @description
* SCIMMY provides a singleton class, `SCIMMY.Messages`, that includes tools for constructing and
* consuming SCIM-compliant data messages to be sent to, or received from, a SCIM service provider.
*/
export default class Messages {
static Error = ErrorMessage;
/**
* @type {typeof SCIMMY.Messages.ErrorResponse}
* @ignore
*/
static Error = ErrorResponse;
static ErrorResponse = ErrorResponse;
static ListResponse = ListResponse;
static PatchOp = PatchOp;
static BulkRequest = BulkRequest;
Expand Down
Loading

0 comments on commit 75dc4f5

Please sign in to comment.