Skip to content

Commit

Permalink
Sm/message-transpilation (#758)
Browse files Browse the repository at this point in the history
* chore: wip

* fix: add missing message

* feat: compile messages via ttypescript

* docs: better explanatory comments

* ci: wireit

* chore: file cleanup

* fix: use cwd for use by other repos

* style: additional comments and comment cleanup

* refactor: remove useless class

* fix: remove one more leftover from the Key class

* chore: bump copyright data and the plugin that managages that
  • Loading branch information
mshanemc authored Jan 26, 2023
1 parent 03de2e3 commit 31fc950
Show file tree
Hide file tree
Showing 8 changed files with 489 additions and 375 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ coverage
docs

# -- CLEAN ALL
*.tsbuildinfo
.eslintcache
.wireit
node_modules

# --
Expand Down
8 changes: 8 additions & 0 deletions .sfdevrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"test": {
"testsPath": "test/**/*Test.ts"
},
"wireit": {
"compile": {
"command": "ttsc -p . --pretty --incremental",
"files": ["src/**/*.ts", "tsconfig.json", "messages", "messageTransformer"],
"output": ["lib/**", "*.tsbuildinfo"],
"clean": "if-file-deleted"
}
}
}
93 changes: 93 additions & 0 deletions messageTransformer/messageTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/* eslint-disable no-console */
/* eslint-disable complexity */

import * as ts from 'typescript';
import { Messages, StoredMessageMap } from '../src/messages';

/**
*
* @experimental
* transforms `messages` references from dynamic run-time to static compile-time values
*/
const transformer = (program: ts.Program, pluginOptions: {}) => {
Messages.importMessagesDirectory(process.cwd());
const transformerFactory: ts.TransformerFactory<ts.SourceFile> = (context) => {
return (sourceFile) => {
// if there are no messages, no transformation is needed
if (
!sourceFile.statements.some((i) => ts.isImportDeclaration(i) && i.importClause?.getText().includes('Messages'))
) {
return sourceFile;
}

const visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
if (ts.isExpressionStatement(node) && node.getText().includes('importMessagesDirectory')) {
// importMessagesDirectory now happens at compile, not in runtime
// returning undefined removes the node
return undefined;
}
if (
// transform a runtime load call into hardcoded messages values
// const foo = Messages.load|loadMessages('pluginName', 'messagesFile' ...) =>
// const foo = new Messages('pluginName', 'messagesFile', new Map([['key', 'value']]))
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.expression.getText() === 'Messages' &&
node.expression.name.getText().includes('load')
) {
// we always want the first two arguments, which are the plugin name and the messages file name
const arrayMembers = node.arguments.slice(0, 2);
const arrayMembersText = arrayMembers.map(getTextWithoutQuotes);

// Messages doesn't care whether you call messages.load or loadMessages, it loads the whole file
const messagesInstance = Messages.loadMessages(arrayMembersText[0], arrayMembersText[1]);
return context.factory.createNewExpression(node.expression.expression, undefined, [
arrayMembers[0],
arrayMembers[1],
context.factory.createNewExpression(context.factory.createIdentifier('Map'), undefined, [
messageMapToHardcodedMap(messagesInstance.messages),
]),
]);
}
// it might be a node that contains one of the things we're interested in, so keep digging
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sourceFile, visitor);
};
};
return transformerFactory;
};

export default transformer;

const getTextWithoutQuotes = (node: ts.Node): string => node.getText().replace(/'/g, '');

/** turn a loaded message map into */
const messageMapToHardcodedMap = (messages: StoredMessageMap): ts.ArrayLiteralExpression => {
return ts.factory.createArrayLiteralExpression(
Array.from(messages).map(([key, value]) => {
// case 1: string
if (typeof value === 'string') {
return ts.factory.createArrayLiteralExpression([
ts.factory.createStringLiteral(key),
ts.factory.createStringLiteral(value),
]);
} else if (Array.isArray(value)) {
// case 2: string[]
return ts.factory.createArrayLiteralExpression([
ts.factory.createStringLiteral(key),
ts.factory.createArrayLiteralExpression(value.map((v) => ts.factory.createStringLiteral(v))),
]);
} else {
// turn the object into a map and recurse!
return messageMapToHardcodedMap(new Map(Object.entries(value)));
}
})
);
};
4 changes: 4 additions & 0 deletions messages/scratchOrgInfoGenerator.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ The ancestor package version [%s] specified in the sfdx-project.json file may ex
# AncestorIdVersionMismatchError

The ancestorVersion in sfdx-project.json is not the version expected for the ancestorId you supplied. ancestorVersion %s. ancestorID %s."

# unsupportedSnapshotOrgCreateOptions

Org snapshots don’t support one or more options you specified: %s
86 changes: 75 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@
"types": "lib/exported.d.ts",
"license": "BSD-3-Clause",
"scripts": {
"build": "sf-build",
"build": "wireit",
"ci-docs": "yarn sf-ci-docs",
"clean": "sf-clean",
"clean-all": "sf-clean all",
"compile": "sf-compile",
"compile": "wireit",
"docs": "sf-docs",
"format": "sf-format",
"lint": "sf-lint",
"format": "wireit",
"lint": "wireit",
"lint-fix": "yarn sf-lint --fix",
"postcompile": "tsc -p test && tsc -p typedocExamples",
"postcompile": "tsc -p typedocExamples",
"prepack": "sf-prepack",
"prepare": "sf-install",
"pretest": "sf-compile-test",
"test": "sf-test"
"test": "wireit",
"test:compile": "wireit",
"test:only": "wireit"
},
"keywords": [
"force",
Expand All @@ -31,7 +32,8 @@
"docs",
"lib",
"messages",
"!lib/**/*.map"
"!lib/**/*.map",
"messageTransformer/messageTransformer.ts"
],
"dependencies": {
"@salesforce/bunyan": "^2.0.0",
Expand All @@ -54,7 +56,7 @@
},
"devDependencies": {
"@salesforce/dev-config": "^3.0.1",
"@salesforce/dev-scripts": "^3.1.0",
"@salesforce/dev-scripts": "^4.0.0-beta.7",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/ts-sinon": "^1.4.4",
"@types/archiver": "^5.3.1",
Expand Down Expand Up @@ -86,13 +88,75 @@
"shelljs": "0.8.5",
"sinon": "^14.0.2",
"ts-node": "^10.4.0",
"typescript": "^4.9.4"
"ttypescript": "^1.5.15",
"typescript": "^4.9.4",
"wireit": "^0.9.3"
},
"repository": {
"type": "git",
"url": "https://github.com/forcedotcom/sfdx-core.git"
},
"publishConfig": {
"access": "public"
},
"wireit": {
"build": {
"dependencies": [
"compile",
"lint"
]
},
"compile": {
"command": "ttsc -p . --pretty --incremental",
"files": [
"src/**/*.ts",
"tsconfig.json",
"messages",
"messageTransformer"
],
"output": [
"lib/**",
"*.tsbuildinfo"
],
"clean": "if-file-deleted"
},
"format": {
"command": "prettier --write \"+(src|test|schemas)/**/*.+(ts|js|json)|command-snapshot.json\""
},
"lint": {
"command": "eslint --color --cache --cache-location .eslintcache",
"files": [
"src/**/*.ts",
"test/**/*.ts",
".eslintignore",
".eslintrc.js"
],
"output": []
},
"test:compile": {
"command": "tsc -p \"./test\" --pretty",
"files": [
"test/**/*.ts",
"tsconfig.json",
"test/tsconfig.json"
],
"output": []
},
"test:only": {
"command": "nyc mocha \"test/**/*Test.ts\"",
"files": [
"test/**/*.ts",
"src/**/*.ts",
"tsconfig.json",
"test/tsconfig.json"
],
"output": []
},
"test": {
"dependencies": [
"test:only",
"test:compile"
]
}
}
}
}
65 changes: 25 additions & 40 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,13 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import {
AnyJson,
asString,
ensureJsonMap,
ensureString,
isArray,
isJsonMap,
isObject,
Optional,
} from '@salesforce/ts-types';
import { NamedError, upperFirst } from '@salesforce/kit';
import { AnyJson, asString, ensureJsonMap, ensureString, isJsonMap, isObject } from '@salesforce/ts-types';
import { ensureArray, NamedError, upperFirst } from '@salesforce/kit';
import { SfError } from './sfError';

export type Tokens = Array<string | boolean | number | null | undefined>;

class Key {
public constructor(private packageName: string, private bundleName: string) {}

public toString(): string {
return `${this.packageName}:${this.bundleName}`;
}
}
const getKey = (packageName: string, bundleName: string): string => `${packageName}:${bundleName}`;

export type StructuredMessage = {
message: string;
Expand Down Expand Up @@ -225,7 +210,7 @@ export class Messages<T extends string> {
* @param locale The locale.
* @param messages The messages. Can not be modified once created.
*/
public constructor(bundleName: string, locale: string, private messages: StoredMessageMap) {
public constructor(bundleName: string, locale: string, public readonly messages: StoredMessageMap) {
this.bundleName = bundleName;
this.locale = locale;
}
Expand Down Expand Up @@ -256,7 +241,7 @@ export class Messages<T extends string> {
* @param loader The loader function.
*/
public static setLoaderFunction(packageName: string, bundle: string, loader: LoaderFunction<string>): void {
this.loaders.set(new Key(packageName, bundle).toString(), loader);
this.loaders.set(getKey(packageName, bundle), loader);
}

/**
Expand Down Expand Up @@ -401,17 +386,17 @@ export class Messages<T extends string> {
* @param bundleName The name of the bundle to load.
*/
public static loadMessages(packageName: string, bundleName: string): Messages<string> {
const key = new Key(packageName, bundleName);
let messages: Optional<Messages<string>>;
const key = getKey(packageName, bundleName);
let messages: Messages<string> | undefined;

if (this.isCached(packageName, bundleName)) {
messages = this.bundles.get(key.toString());
} else if (this.loaders.has(key.toString())) {
const loader = this.loaders.get(key.toString());
messages = this.bundles.get(key);
} else if (this.loaders.has(key)) {
const loader = this.loaders.get(key);
if (loader) {
messages = loader(Messages.getLocale());
this.bundles.set(key.toString(), messages);
messages = this.bundles.get(key.toString());
this.bundles.set(key, messages);
messages = this.bundles.get(key);
}
}

Expand All @@ -420,7 +405,7 @@ export class Messages<T extends string> {
}

// Don't use messages inside messages
throw new NamedError('MissingBundleError', `Missing bundle ${key.toString()} for locale ${Messages.getLocale()}.`);
throw new NamedError('MissingBundleError', `Missing bundle ${key} for locale ${Messages.getLocale()}.`);
}

/**
Expand All @@ -444,17 +429,17 @@ export class Messages<T extends string> {
* @param keys The message keys that will be used.
*/
public static load<T extends string>(packageName: string, bundleName: string, keys: [T, ...T[]]): Messages<T> {
const key = new Key(packageName, bundleName);
let messages: Optional<Messages<T>>;
const key = getKey(packageName, bundleName);
let messages: Messages<T> | undefined;

if (this.isCached(packageName, bundleName)) {
messages = this.bundles.get(key.toString());
} else if (this.loaders.has(key.toString())) {
const loader = this.loaders.get(key.toString());
messages = this.bundles.get(key);
} else if (this.loaders.has(key)) {
const loader = this.loaders.get(key);
if (loader) {
messages = loader(Messages.getLocale());
this.bundles.set(key.toString(), messages);
messages = this.bundles.get(key.toString());
this.bundles.set(key, messages);
messages = this.bundles.get(key);
}
}

Expand All @@ -476,7 +461,7 @@ export class Messages<T extends string> {
}

// Don't use messages inside messages
throw new NamedError('MissingBundleError', `Missing bundle ${key.toString()} for locale ${Messages.getLocale()}.`);
throw new NamedError('MissingBundleError', `Missing bundle ${key} for locale ${Messages.getLocale()}.`);
}

/**
Expand All @@ -486,7 +471,7 @@ export class Messages<T extends string> {
* @param bundleName The bundle name.
*/
public static isCached(packageName: string, bundleName: string): boolean {
return this.bundles.has(new Key(packageName, bundleName).toString());
return this.bundles.has(getKey(packageName, bundleName));
}

/**
Expand Down Expand Up @@ -671,15 +656,15 @@ export class Messages<T extends string> {
}
}

if (!map.has(key)) {
const msg = map.get(key);
if (!msg) {
// Don't use messages inside messages
throw new NamedError(
'MissingMessageError',
`Missing message ${this.bundleName}:${key} for locale ${Messages.getLocale()}.`
);
}
const msg = map.get(key);
const messages = (isArray(msg) ? msg : [msg]) as string[];
const messages = ensureArray(msg);
return messages.map((message) => {
ensureString(message);
return util.format(message, ...tokens);
Expand Down
Loading

0 comments on commit 31fc950

Please sign in to comment.