Improved the translation generator. It now generates a up2date translation list
This commit is contained in:
parent
ff610bf3ee
commit
29639ca836
12 changed files with 742 additions and 730 deletions
|
@ -1,8 +1,5 @@
|
||||||
import {TranslationEntry} from "./generator";
|
import {TranslationEntry} from "./generator";
|
||||||
|
|
||||||
export interface Configuration {
|
|
||||||
|
|
||||||
}
|
|
||||||
export interface File {
|
export interface File {
|
||||||
content: string;
|
content: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -11,15 +8,15 @@ export interface File {
|
||||||
/* Well my IDE hates me and does not support groups. By default with ES6 groups are supported... nvm */
|
/* Well my IDE hates me and does not support groups. By default with ES6 groups are supported... nvm */
|
||||||
//const regex = /{{ *tr *(?<message_expression>(("([^"]|\\")*")|('([^']|\\')*')|(`([^`]|\\`)+`)|( *\+ *)?)+) *\/ *}}/;
|
//const regex = /{{ *tr *(?<message_expression>(("([^"]|\\")*")|('([^']|\\')*')|(`([^`]|\\`)+`)|( *\+ *)?)+) *\/ *}}/;
|
||||||
const regex = /{{ *tr *((("([^"]|\\")*")|('([^']|\\')*')|(`([^`]|\\`)+`)|([\n ]*\+[\n ]*)?)+) *\/ *}}/;
|
const regex = /{{ *tr *((("([^"]|\\")*")|('([^']|\\')*')|(`([^`]|\\`)+`)|([\n ]*\+[\n ]*)?)+) *\/ *}}/;
|
||||||
export function generate(config: Configuration, file: File) : TranslationEntry[] {
|
export function extractJsRendererTranslations(file: File) : TranslationEntry[] {
|
||||||
let result: TranslationEntry[] = [];
|
let result: TranslationEntry[] = [];
|
||||||
|
|
||||||
const lines = file.content.split('\n');
|
const lines = file.content.split('\n');
|
||||||
let match: RegExpExecArray;
|
let match: RegExpExecArray;
|
||||||
let base_index = 0;
|
let baseIndex = 0;
|
||||||
|
|
||||||
while(match = regex.exec(file.content.substr(base_index))) {
|
while(match = regex.exec(file.content.substr(baseIndex))) {
|
||||||
let expression = ((<any>match).groups || {})["message_expression"] || match[1];
|
let expression = (match.groups || {})["message_expression"] || match[1];
|
||||||
//expression = expression.replace(/\n/g, "\\n");
|
//expression = expression.replace(/\n/g, "\\n");
|
||||||
|
|
||||||
let message;
|
let message;
|
||||||
|
@ -27,16 +24,18 @@ export function generate(config: Configuration, file: File) : TranslationEntry[]
|
||||||
message = eval(expression);
|
message = eval(expression);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to evaluate expression:\n%s", expression);
|
console.error("Failed to evaluate expression:\n%s", expression);
|
||||||
base_index += match.index + match[0].length;
|
baseIndex += match.index + match[0].length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let character = base_index + match.index;
|
let character = baseIndex + match.index;
|
||||||
let line;
|
let line;
|
||||||
|
|
||||||
for(line = 0; line < lines.length; line++) {
|
for(line = 0; line < lines.length; line++) {
|
||||||
const length = lines[line].length + 1;
|
const length = lines[line].length + 1;
|
||||||
if(length > character) break;
|
if(length > character) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
character -= length;
|
character -= length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +47,7 @@ export function generate(config: Configuration, file: File) : TranslationEntry[]
|
||||||
type: "js-template"
|
type: "js-template"
|
||||||
});
|
});
|
||||||
|
|
||||||
base_index += match.index + match[0].length;
|
baseIndex += match.index + match[0].length;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
19
tools/trgen/JsRendererTranslationLoader.ts
Normal file
19
tools/trgen/JsRendererTranslationLoader.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {extractJsRendererTranslations} from "./JsRendererGenerator";
|
||||||
|
import {deltaTranslations} from "./TsTransformer";
|
||||||
|
|
||||||
|
export default function (source) {
|
||||||
|
const options = this.getOptions({
|
||||||
|
translations: { type: "array" }
|
||||||
|
});
|
||||||
|
source = typeof source === "object" ? source.toString() : source;
|
||||||
|
|
||||||
|
const timestampBegin = Date.now();
|
||||||
|
const translations = extractJsRendererTranslations({
|
||||||
|
name: this.resourcePath,
|
||||||
|
content: source
|
||||||
|
});
|
||||||
|
const timestampEnd = Date.now();
|
||||||
|
|
||||||
|
deltaTranslations(options.translations, this.resourcePath, timestampEnd - timestampBegin, translations);
|
||||||
|
return source;
|
||||||
|
};
|
415
tools/trgen/TsGenerator.ts
Normal file
415
tools/trgen/TsGenerator.ts
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
import * as ts from "typescript";
|
||||||
|
import sha256 from "sha256";
|
||||||
|
import {SyntaxKind} from "typescript";
|
||||||
|
import {TranslationEntry} from "./generator";
|
||||||
|
|
||||||
|
const getSourceLocation = (node: ts.Node) => {
|
||||||
|
const sf = node.getSourceFile();
|
||||||
|
let { line, character } = sf ? sf.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
|
||||||
|
return `${(sf || {fileName: "unknown"}).fileName} (${line + 1},${character + 1})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function report(node: ts.Node, message: string) {
|
||||||
|
console.log(`${getSourceLocation(node)}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUniqueCheck(config: Configuration, source_file: ts.SourceFile, variable: ts.Expression, variables: { name: string, node: ts.Node }[]) : ts.Node[] {
|
||||||
|
const nodes: ts.Node[] = [], blockedNodes: ts.Statement[] = [];
|
||||||
|
|
||||||
|
const nodePath = (node: ts.Node) => {
|
||||||
|
const sf = node.getSourceFile();
|
||||||
|
let { line, character } = sf ? sf.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
|
||||||
|
return `${(sf || {fileName: "unknown"}).fileName} (${line + 1},${character + 1})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createError = (variable_name: ts.Expression, variable_path: ts.Expression, other_path: ts.Expression) => {
|
||||||
|
return [
|
||||||
|
ts.createLiteral("Translation with generated name \""),
|
||||||
|
variable_name,
|
||||||
|
ts.createLiteral("\" already exists!\nIt has been already defined here: "),
|
||||||
|
other_path,
|
||||||
|
ts.createLiteral("\nAttempted to redefine here: "),
|
||||||
|
variable_path,
|
||||||
|
ts.createLiteral("\nRegenerate and/or fix your program!")
|
||||||
|
].reduce((a, b) => ts.createBinary(a, SyntaxKind.PlusToken, b));
|
||||||
|
};
|
||||||
|
|
||||||
|
let declarationsFile: ts.Expression;
|
||||||
|
const uniqueCheckLabelName = "unique_translation_check";
|
||||||
|
|
||||||
|
/* initialization */
|
||||||
|
{
|
||||||
|
const declarations = ts.createElementAccess(variable, ts.createLiteral(config.variables.declarations));
|
||||||
|
nodes.push(ts.createAssignment(declarations, ts.createBinary(declarations, SyntaxKind.BarBarToken, ts.createAssignment(declarations, ts.createObjectLiteral()))));
|
||||||
|
|
||||||
|
declarationsFile = ts.createElementAccess(variable, ts.createLiteral(config.variables.declareFiles));
|
||||||
|
nodes.push(ts.createAssignment(declarationsFile, ts.createBinary(declarationsFile, SyntaxKind.BarBarToken, ts.createAssignment(declarationsFile, ts.createObjectLiteral()))));
|
||||||
|
|
||||||
|
variable = declarations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* test file already loaded */
|
||||||
|
{
|
||||||
|
const uniqueId = sha256(source_file.fileName + " | " + (Date.now() / 1000));
|
||||||
|
const property = ts.createElementAccess(declarationsFile, ts.createLiteral(uniqueId));
|
||||||
|
|
||||||
|
const ifCondition = ts.createBinary(property, SyntaxKind.ExclamationEqualsEqualsToken, ts.createIdentifier("undefined"));
|
||||||
|
//
|
||||||
|
let ifThen: ts.Block;
|
||||||
|
{
|
||||||
|
const elements: ts.Statement[] = [];
|
||||||
|
|
||||||
|
const console = ts.createIdentifier("console.warn");
|
||||||
|
elements.push(ts.createCall(console, [], [ts.createLiteral("This file has already been loaded!\nAre you executing scripts twice?") as any]) as any);
|
||||||
|
elements.push(ts.createBreak(uniqueCheckLabelName));
|
||||||
|
|
||||||
|
ifThen = ts.createBlock(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ifElse = ts.createAssignment(property, ts.createLiteral(uniqueId));
|
||||||
|
blockedNodes.push(ts.createIf(ifCondition, ifThen, ifElse as any));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* test if variable has been defined somewhere else */
|
||||||
|
{
|
||||||
|
const forVariableName = ts.createLoopVariable();
|
||||||
|
const forVariablePath = ts.createLoopVariable();
|
||||||
|
const forDeclaration = ts.createVariableDeclarationList([ts.createVariableDeclaration(ts.createObjectBindingPattern([
|
||||||
|
ts.createBindingElement(undefined, config.optimized ? "n": "name", forVariableName, undefined),
|
||||||
|
ts.createBindingElement(undefined, config.optimized ? "p": "path", forVariablePath, undefined)])
|
||||||
|
, undefined, undefined)]);
|
||||||
|
|
||||||
|
let forBlock: ts.Statement;
|
||||||
|
{ //Create the for block
|
||||||
|
const elements: ts.Statement[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
const property = ts.createElementAccess(variable, forVariableName);
|
||||||
|
const ifCondition = ts.createBinary(property, SyntaxKind.ExclamationEqualsEqualsToken, ts.createIdentifier("undefined"));
|
||||||
|
|
||||||
|
//
|
||||||
|
const ifThen = ts.createThrow(createError(forVariableName, forVariablePath, property));
|
||||||
|
const ifElse = ts.createAssignment(property, forVariablePath);
|
||||||
|
const ifValid = ts.createIf(ifCondition, ifThen, ifElse as any);
|
||||||
|
|
||||||
|
elements.push(ifValid);
|
||||||
|
|
||||||
|
forBlock = ts.createBlock(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = ts.createForOf(undefined,
|
||||||
|
forDeclaration, ts.createArrayLiteral(
|
||||||
|
[...variables.map(e => ts.createObjectLiteral([
|
||||||
|
ts.createPropertyAssignment(config.optimized ? "n": "name", ts.createLiteral(e.name)),
|
||||||
|
ts.createPropertyAssignment(config.optimized ? "p": "path", ts.createLiteral(nodePath(e.node)))
|
||||||
|
]))
|
||||||
|
])
|
||||||
|
, forBlock);
|
||||||
|
block = ts.addSyntheticLeadingComment(block, SyntaxKind.MultiLineCommentTrivia, "Auto generated helper for testing if the translation keys are unique", true);
|
||||||
|
blockedNodes.push(block);
|
||||||
|
}
|
||||||
|
return [...nodes, ts.createLabel(uniqueCheckLabelName, ts.createBlock(blockedNodes))];
|
||||||
|
}
|
||||||
|
|
||||||
|
let globalIdIndex = 0, globalIdTimestamp = Date.now();
|
||||||
|
export function transform(config: Configuration, context: ts.TransformationContext, sourceFile: ts.SourceFile) : TransformResult {
|
||||||
|
const cache: VolatileTransformConfig = {} as any;
|
||||||
|
cache.translations = [];
|
||||||
|
|
||||||
|
config.variables = (config.variables || {}) as any;
|
||||||
|
config.variables.base = config.variables.base || (config.optimized ? "__tr" : "_translations");
|
||||||
|
config.variables.declareFiles = config.variables.declareFiles || (config.optimized ? "f" : "declare_files");
|
||||||
|
config.variables.declarations = config.variables.declarations || (config.optimized ? "d" : "definitions");
|
||||||
|
|
||||||
|
//Initialize nodes
|
||||||
|
const extraNodes = [];
|
||||||
|
{
|
||||||
|
cache.nodes = {} as any;
|
||||||
|
if(config.useWindow) {
|
||||||
|
const window = ts.createIdentifier("window");
|
||||||
|
const translationMap = ts.createPropertyAccess(window, ts.createIdentifier(config.variables.base));
|
||||||
|
const newTranslations = ts.createAssignment(translationMap, ts.createObjectLiteral());
|
||||||
|
|
||||||
|
extraNodes.push(ts.createParen(
|
||||||
|
ts.createBinary(translationMap, ts.SyntaxKind.BarBarToken, newTranslations)
|
||||||
|
));
|
||||||
|
|
||||||
|
cache.nodes = {
|
||||||
|
translationMap: translationMap
|
||||||
|
};
|
||||||
|
} else if(config.module) {
|
||||||
|
cache.nodes = {
|
||||||
|
translationMap: ts.createIdentifier(config.variables.base)
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodes.push((
|
||||||
|
ts.createVariableDeclarationList([
|
||||||
|
ts.createVariableDeclaration(config.variables.base, undefined, ts.createObjectLiteral())
|
||||||
|
], ts.NodeFlags.Const)
|
||||||
|
), ts.createToken(SyntaxKind.SemicolonToken));
|
||||||
|
} else {
|
||||||
|
const variableMap = ts.createIdentifier(config.variables.base);
|
||||||
|
const inlineIf = ts.createBinary(
|
||||||
|
ts.createBinary(
|
||||||
|
ts.createTypeOf(variableMap),
|
||||||
|
SyntaxKind.ExclamationEqualsEqualsToken,
|
||||||
|
ts.createLiteral("undefined")
|
||||||
|
),
|
||||||
|
ts.SyntaxKind.BarBarToken,
|
||||||
|
ts.createAssignment(variableMap, ts.createObjectLiteral())
|
||||||
|
);
|
||||||
|
|
||||||
|
cache.nodes = {
|
||||||
|
translationMap: variableMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodes.push(inlineIf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatedNames: { name: string, node: ts.Node }[] = [];
|
||||||
|
let generatorBase = 0;
|
||||||
|
|
||||||
|
const generateUniqueName = config => {
|
||||||
|
if(config.module) {
|
||||||
|
return "_" + generatorBase++;
|
||||||
|
} else {
|
||||||
|
return "_" + globalIdTimestamp + "-" + ++globalIdIndex;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.nameGenerator = (config, node) => {
|
||||||
|
const name = generateUniqueName(config);
|
||||||
|
generatedNames.push({name: name, node: node});
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.tsxNameGenerator = generateUniqueName;
|
||||||
|
|
||||||
|
function visit(node: ts.Node): ts.Node {
|
||||||
|
node = ts.visitEachChild(node, visit, context);
|
||||||
|
return visitNode(config, cache, node, sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceFile = ts.visitNode(sourceFile, visit);
|
||||||
|
if(!config.module) {
|
||||||
|
/* we don't need a unique check because we're just in our scope */
|
||||||
|
extraNodes.push(...generateUniqueCheck(config, sourceFile, cache.nodes.translationMap, generatedNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!config.cacheTranslations) {
|
||||||
|
sourceFile = ts.updateSourceFileNode(sourceFile, [
|
||||||
|
...extraNodes,
|
||||||
|
...sourceFile.statements
|
||||||
|
], sourceFile.isDeclarationFile, sourceFile.referencedFiles, sourceFile.typeReferenceDirectives, sourceFile.hasNoDefaultLib, sourceFile.referencedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: sourceFile,
|
||||||
|
translations: cache.translations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateJsxCacheKey = (cache: VolatileTransformConfig, config: Configuration, element: ts.JsxElement) => ts.updateJsxElement(
|
||||||
|
element,
|
||||||
|
ts.updateJsxOpeningElement(
|
||||||
|
element.openingElement,
|
||||||
|
element.openingElement.tagName,
|
||||||
|
element.openingElement.typeArguments,
|
||||||
|
ts.updateJsxAttributes(element.openingElement.attributes, [
|
||||||
|
...element.openingElement.attributes.properties,
|
||||||
|
ts.createJsxAttribute(ts.createIdentifier("__cacheKey"), ts.createStringLiteral(cache.tsxNameGenerator(config)))
|
||||||
|
])
|
||||||
|
),
|
||||||
|
element.children,
|
||||||
|
element.closingElement
|
||||||
|
);
|
||||||
|
|
||||||
|
export function visitNode(config: Configuration, cache: VolatileTransformConfig, node: ts.Node, sourceFile: ts.SourceFile) : ts.Node {
|
||||||
|
if(config.verbose) {
|
||||||
|
console.log("Process %s", SyntaxKind[node.kind]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!node.getSourceFile()) {
|
||||||
|
/* Node is already artificial */
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ts.isCallExpression(node)) {
|
||||||
|
const call = node as ts.CallExpression;
|
||||||
|
const callName = call.expression["escapedText"] as string;
|
||||||
|
if(callName !== "tr" && callName !== "useTr") {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(call.arguments.length > 1) {
|
||||||
|
throw new Error(getSourceLocation(call) + ": tr(...) has been called with an invalid arguments (" + (call.arguments.length === 0 ? "too few" : "too many") + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullText = call.getFullText(sourceFile);
|
||||||
|
if(fullText && fullText.indexOf("@tr-ignore") !== -1) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const object = <ts.StringLiteral>call.arguments[0];
|
||||||
|
if(object.kind != SyntaxKind.StringLiteral) {
|
||||||
|
if(call.getSourceFile()) {
|
||||||
|
throw new Error(getSourceLocation(call) + ": Ignoring tr call because given argument isn't of type string literal. (" + SyntaxKind[object.kind] + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
report(call, "Ignoring tr call because given argument isn't of type string literal. (" + SyntaxKind[object.kind] + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(config.verbose) {
|
||||||
|
console.log("Message: %o", object.text || object.getText(sourceFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
let { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
||||||
|
cache.translations.push({
|
||||||
|
message: object.text || object.getText(sourceFile),
|
||||||
|
line: line,
|
||||||
|
character: character,
|
||||||
|
filename: sourceFile.fileName,
|
||||||
|
type: "call"
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!config.cacheTranslations) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableName = ts.createIdentifier(cache.nameGenerator(config, node, object.text || object.getText(sourceFile)));
|
||||||
|
const variableInit = ts.createPropertyAccess(cache.nodes.translationMap, variableName);
|
||||||
|
|
||||||
|
const variable = ts.createPropertyAccess(cache.nodes.translationMap, variableName);
|
||||||
|
const newVariable = ts.createAssignment(variable, call);
|
||||||
|
|
||||||
|
return ts.createBinary(variableInit, ts.SyntaxKind.BarBarToken, newVariable);
|
||||||
|
} else if(node.kind === SyntaxKind.JsxElement) {
|
||||||
|
const element = node as ts.JsxElement;
|
||||||
|
const tag = element.openingElement.tagName as ts.Identifier;
|
||||||
|
|
||||||
|
if(tag.kind !== SyntaxKind.Identifier) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties: any = {};
|
||||||
|
element.openingElement.attributes.properties.forEach((e: ts.JsxAttribute) => {
|
||||||
|
if(e.kind !== SyntaxKind.JsxAttribute) {
|
||||||
|
throw new Error(getSourceLocation(e) + ": Invalid jsx attribute kind " + SyntaxKind[e.kind]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.name.kind !== SyntaxKind.Identifier) {
|
||||||
|
throw new Error(getSourceLocation(e) + ": Key isn't an identifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
properties[e.name.escapedText as string] = e.initializer;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(tag.escapedText === "Translatable") {
|
||||||
|
if('trIgnore' in properties && properties.trIgnore.kind === SyntaxKind.JsxExpression) {
|
||||||
|
const ignoreAttribute = properties.trIgnore as ts.JsxExpression;
|
||||||
|
if(ignoreAttribute.expression.kind === SyntaxKind.TrueKeyword) {
|
||||||
|
return node;
|
||||||
|
} else if(ignoreAttribute.expression.kind !== SyntaxKind.FalseKeyword) {
|
||||||
|
throw new Error(getSourceLocation(ignoreAttribute) + ": Invalid attribute value of type " + SyntaxKind[ignoreAttribute.expression.kind]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(element.children.length < 1) {
|
||||||
|
throw new Error(getSourceLocation(element) + ": Element has been called with an invalid arguments (too few)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = element.children.map(element => {
|
||||||
|
if(element.kind === SyntaxKind.JsxText) {
|
||||||
|
return element.text;
|
||||||
|
} else if(element.kind === SyntaxKind.JsxSelfClosingElement) {
|
||||||
|
if(element.tagName.kind !== SyntaxKind.Identifier) {
|
||||||
|
throw new Error(getSourceLocation(element.tagName) + ": Expected a JsxSelfClosingElement, but received " + SyntaxKind[element.tagName.kind]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(element.tagName.escapedText !== "br") {
|
||||||
|
throw new Error(getSourceLocation(element.tagName) + ": Expected a br element, but received " + element.tagName.escapedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\n";
|
||||||
|
}
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
let { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
||||||
|
cache.translations.push({
|
||||||
|
message: text,
|
||||||
|
line: line,
|
||||||
|
character: character,
|
||||||
|
filename: sourceFile.fileName,
|
||||||
|
type: "jsx-translatable"
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!config.cacheTranslations) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateJsxCacheKey(cache, config, element);
|
||||||
|
} else if(tag.escapedText === "VariadicTranslatable") {
|
||||||
|
if(!('text' in properties)) {
|
||||||
|
throw new Error(getSourceLocation(element) + ": Missing text to translate");
|
||||||
|
}
|
||||||
|
|
||||||
|
const textAttribute = properties["text"] as ts.JsxExpression;
|
||||||
|
if(textAttribute.kind !== SyntaxKind.JsxExpression) {
|
||||||
|
throw new Error(getSourceLocation(element) + ": Text attribute has an invalid type. Expected JsxExpression but received " + SyntaxKind[textAttribute.kind]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(textAttribute.expression.kind !== SyntaxKind.StringLiteral) {
|
||||||
|
throw new Error(getSourceLocation(element) + ": Text attribute value isn't a string literal. Expected StringLiteral but received " + SyntaxKind[textAttribute.expression.kind]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const literal = textAttribute.expression as ts.StringLiteral;
|
||||||
|
|
||||||
|
let { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
||||||
|
cache.translations.push({
|
||||||
|
message: literal.text,
|
||||||
|
line: line,
|
||||||
|
character: character,
|
||||||
|
filename: sourceFile.fileName,
|
||||||
|
type: "jsx-variadic-translatable"
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!config.cacheTranslations) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateJsxCacheKey(cache, config, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
export interface Configuration {
|
||||||
|
useWindow?: boolean;
|
||||||
|
cacheTranslations?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
|
||||||
|
optimized?: boolean;
|
||||||
|
module?: boolean;
|
||||||
|
|
||||||
|
variables?: {
|
||||||
|
base: string,
|
||||||
|
declarations: string,
|
||||||
|
declareFiles: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransformResult {
|
||||||
|
node: ts.SourceFile;
|
||||||
|
translations: TranslationEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolatileTransformConfig {
|
||||||
|
nodes: {
|
||||||
|
translationMap: ts.Expression;
|
||||||
|
};
|
||||||
|
|
||||||
|
nameGenerator: (config: Configuration, node: ts.Node, message: string) => string;
|
||||||
|
tsxNameGenerator: (config: Configuration) => string;
|
||||||
|
translations: TranslationEntry[];
|
||||||
|
}
|
52
tools/trgen/TsTransformer.ts
Normal file
52
tools/trgen/TsTransformer.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import * as ts from "typescript";
|
||||||
|
import * as ts_generator from "./TsGenerator";
|
||||||
|
import {TranslationEntry} from "./generator";
|
||||||
|
|
||||||
|
export interface TransformerConfig {
|
||||||
|
verbose: boolean;
|
||||||
|
optimized: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output array for all gathered translations.
|
||||||
|
*/
|
||||||
|
translations?: TranslationEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deltaTranslations = (result: TranslationEntry[], fileName: string, processSpeed: number, newTranslations: TranslationEntry[]) => {
|
||||||
|
let deletedTranslations = 0;
|
||||||
|
for(let index = 0; index < result.length; index++) {
|
||||||
|
if(result[index].filename === fileName) {
|
||||||
|
result.splice(index, 1);
|
||||||
|
index--;
|
||||||
|
deletedTranslations++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(...newTranslations);
|
||||||
|
if(deletedTranslations === 0 && newTranslations.length === 0) {
|
||||||
|
console.log("Processed %s (%dms). No translations found.", fileName, processSpeed);
|
||||||
|
} else if(deletedTranslations === 0) {
|
||||||
|
console.log("Processed %s (%dms). Found %d translations", fileName, processSpeed, newTranslations.length);
|
||||||
|
} else if(newTranslations.length === 0) {
|
||||||
|
console.log("Processed %s (%dms). %d translations deleted.", fileName, processSpeed, deletedTranslations);
|
||||||
|
} else {
|
||||||
|
console.log("Processed %s (%dms). Old translation count: %d New translation count: %d", fileName, processSpeed, deletedTranslations, newTranslations.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTransformer = (program: ts.Program, config: TransformerConfig) : ts.TransformerFactory<ts.SourceFile> => {
|
||||||
|
return ctx => sourceFile => {
|
||||||
|
const timestampBegin = Date.now();
|
||||||
|
const result = ts_generator.transform({
|
||||||
|
module: true,
|
||||||
|
useWindow: false,
|
||||||
|
/* Note: Even though caching might cause less method calls but if the tr method is performant developed it's faster than having the cache lookup */
|
||||||
|
cacheTranslations: false,
|
||||||
|
optimized: config.optimized,
|
||||||
|
}, ctx, sourceFile);
|
||||||
|
const timestampEnd = Date.now();
|
||||||
|
|
||||||
|
deltaTranslations(config.translations || [], sourceFile.fileName, timestampEnd - timestampBegin, result.translations);
|
||||||
|
return result.node;
|
||||||
|
};
|
||||||
|
}
|
55
tools/trgen/WebpackPlugin.ts
Normal file
55
tools/trgen/WebpackPlugin.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import * as ts from "typescript";
|
||||||
|
import {createTransformer, deltaTranslations, TransformerConfig} from "./TsTransformer";
|
||||||
|
import * as webpack from "webpack";
|
||||||
|
import {extractJsRendererTranslations} from "./JsRendererGenerator";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export interface TranslateableWebpackPluginConfig {
|
||||||
|
assetName: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TranslateableWebpackPlugin {
|
||||||
|
private readonly config: TranslateableWebpackPluginConfig;
|
||||||
|
private readonly transformerConfig: TransformerConfig;
|
||||||
|
|
||||||
|
constructor(config: TranslateableWebpackPluginConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.transformerConfig = {
|
||||||
|
optimized: true,
|
||||||
|
translations: [],
|
||||||
|
verbose: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createTypeScriptTransformer(program: ts.Program) : ts.TransformerFactory<ts.Node> {
|
||||||
|
return createTransformer(program, this.transformerConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTemplateLoader() {
|
||||||
|
return {
|
||||||
|
loader: path.join(__dirname, "JsRendererTranslationLoader.js"),
|
||||||
|
options: {
|
||||||
|
translations: this.transformerConfig.translations
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler: webpack.Compiler) {
|
||||||
|
compiler.hooks.emit.tap("TranslateableWebpackPlugin", compilation => {
|
||||||
|
const payload = JSON.stringify(this.transformerConfig.translations);
|
||||||
|
compilation.assets[this.config.assetName] = {
|
||||||
|
size() { return payload.length; },
|
||||||
|
source() { return payload; }
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
compiler.hooks.normalModuleFactory.tap("TranslateableWebpackPlugin", normalModuleFactory => {
|
||||||
|
normalModuleFactory.hooks.resolve.tap("TranslateableWebpackPlugin", resolveData => {
|
||||||
|
if(resolveData.request === "generated-translations") {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import * as ts from "typescript";
|
import * as ts from "typescript";
|
||||||
import * as generator from "./ts_generator";
|
import * as generator from "./TsGenerator";
|
||||||
|
|
||||||
import {readFileSync} from "fs";
|
import {readFileSync} from "fs";
|
||||||
import * as glob from "glob";
|
import * as glob from "glob";
|
||||||
|
@ -7,8 +7,8 @@ import * as path from "path";
|
||||||
|
|
||||||
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
|
const transformer = <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
|
||||||
return generator.transform({
|
return generator.transform({
|
||||||
use_window: false,
|
useWindow: false,
|
||||||
replace_cache: true,
|
cacheTranslations: true,
|
||||||
verbose: true
|
verbose: true
|
||||||
}, context, rootNode as any).node;
|
}, context, rootNode as any).node;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as ts from "typescript";
|
import * as ts from "typescript";
|
||||||
import * as ts_generator from "./ts_generator";
|
import * as ts_generator from "./TsGenerator";
|
||||||
import * as jsrender_generator from "./jsrender_generator";
|
import * as jsrender_generator from "./JsRendererGenerator";
|
||||||
import {readFileSync, writeFileSync} from "fs";
|
import {readFileSync, writeFileSync} from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as glob from "glob";
|
import * as glob from "glob";
|
||||||
|
@ -119,10 +119,11 @@ config.source_files.forEach(file => {
|
||||||
);
|
);
|
||||||
console.log("Compile " + _file);
|
console.log("Compile " + _file);
|
||||||
|
|
||||||
const messages = ts_generator.generate(source, {});
|
throw "not supported";
|
||||||
translations.push(...messages);
|
//const messages = ts_generator.generate(source, {});
|
||||||
|
//translations.push(...messages);
|
||||||
} else if(file_type == ".html") {
|
} else if(file_type == ".html") {
|
||||||
const messages = jsrender_generator.generate({}, {
|
const messages = jsrender_generator.extractJsRendererTranslations({
|
||||||
content: readFileSync(_file).toString(),
|
content: readFileSync(_file).toString(),
|
||||||
name: _file
|
name: _file
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,459 +0,0 @@
|
||||||
import * as ts from "typescript";
|
|
||||||
import {SyntaxKind} from "typescript";
|
|
||||||
import sha256 from "sha256";
|
|
||||||
import {TranslationEntry} from "./generator";
|
|
||||||
|
|
||||||
export function generate(file: ts.SourceFile, config: Configuration) : TranslationEntry[] {
|
|
||||||
let result: TranslationEntry[] = [];
|
|
||||||
|
|
||||||
file.forEachChild(n => _generate(config, n, result));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source_location = (node: ts.Node) => {
|
|
||||||
const sf = node.getSourceFile();
|
|
||||||
let { line, character } = sf ? sf.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
|
|
||||||
return `${(sf || {fileName: "unknown"}).fileName} (${line + 1},${character + 1})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function report(node: ts.Node, message: string) {
|
|
||||||
console.log(`${source_location(node)}: ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _generate(config: Configuration, node: ts.Node, result: TranslationEntry[]) {
|
|
||||||
//console.log("Node: %s", SyntaxKind[node.kind]);
|
|
||||||
|
|
||||||
call_analize:
|
|
||||||
if(ts.isCallExpression(node)) {
|
|
||||||
const call = node as ts.CallExpression;
|
|
||||||
const call_name = call.expression["escapedText"] as string;
|
|
||||||
|
|
||||||
if(call_name != "tr") {
|
|
||||||
break call_analize;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.dir(call_name);
|
|
||||||
console.log("Parameters: %o", call.arguments.length);
|
|
||||||
if(call.arguments.length > 1) {
|
|
||||||
report(call, "Invalid argument count");
|
|
||||||
break call_analize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const object = <ts.StringLiteral>call.arguments[0];
|
|
||||||
if(object.kind != SyntaxKind.StringLiteral) {
|
|
||||||
report(call, "Invalid argument: " + SyntaxKind[object.kind]);
|
|
||||||
break call_analize;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Message: %o", object.text);
|
|
||||||
|
|
||||||
//FIXME
|
|
||||||
if(config.replace_cache) {
|
|
||||||
console.log("Update!");
|
|
||||||
ts.updateCall(call, call.expression, call.typeArguments, [ts.createLiteral("PENIS!")]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
|
|
||||||
result.push({
|
|
||||||
filename: node.getSourceFile().fileName,
|
|
||||||
line: line,
|
|
||||||
character: character,
|
|
||||||
message: object.text,
|
|
||||||
type: "call"
|
|
||||||
});
|
|
||||||
} else if(node.kind === SyntaxKind.JsxElement) {
|
|
||||||
const element = node as ts.JsxElement;
|
|
||||||
const tag = element.openingElement.tagName as ts.Identifier;
|
|
||||||
|
|
||||||
if(tag.kind !== SyntaxKind.Identifier)
|
|
||||||
break call_analize;
|
|
||||||
|
|
||||||
if(tag.escapedText === "Translatable") {
|
|
||||||
if(element.children.length !== 1) {
|
|
||||||
report(element, "Invalid child count: " + element.children.length);
|
|
||||||
break call_analize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = element.children[0] as ts.JsxText;
|
|
||||||
if(text.kind != SyntaxKind.JsxText) {
|
|
||||||
report(element, "Invalid child type " + SyntaxKind[text.kind]);
|
|
||||||
break call_analize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
|
|
||||||
result.push({
|
|
||||||
filename: node.getSourceFile().fileName,
|
|
||||||
line: line,
|
|
||||||
character: character,
|
|
||||||
message: text.text,
|
|
||||||
type: "jsx-translatable"
|
|
||||||
});
|
|
||||||
} else if(tag.escapedText === "VariadicTranslatable") {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.forEachChild(n => _generate(config, n, result));
|
|
||||||
}
|
|
||||||
function generateUniqueCheck(config: Configuration, source_file: ts.SourceFile, variable: ts.Expression, variables: { name: string, node: ts.Node }[]) : ts.Node[] {
|
|
||||||
const nodes: ts.Node[] = [], blocked_nodes: ts.Statement[] = [];
|
|
||||||
|
|
||||||
const node_path = (node: ts.Node) => {
|
|
||||||
const sf = node.getSourceFile();
|
|
||||||
let { line, character } = sf ? sf.getLineAndCharacterOfPosition(node.getStart()) : {line: -1, character: -1};
|
|
||||||
return `${(sf || {fileName: "unknown"}).fileName} (${line + 1},${character + 1})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const create_error = (variable_name: ts.Expression, variable_path: ts.Expression, other_path: ts.Expression) => {
|
|
||||||
return [
|
|
||||||
ts.createLiteral("Translation with generated name \""),
|
|
||||||
variable_name,
|
|
||||||
ts.createLiteral("\" already exists!\nIt has been already defined here: "),
|
|
||||||
other_path,
|
|
||||||
ts.createLiteral("\nAttempted to redefine here: "),
|
|
||||||
variable_path,
|
|
||||||
ts.createLiteral("\nRegenerate and/or fix your program!")
|
|
||||||
].reduce((a, b) => ts.createBinary(a, SyntaxKind.PlusToken, b));
|
|
||||||
};
|
|
||||||
|
|
||||||
let declarations_file: ts.Expression;
|
|
||||||
const unique_check_label_name = "unique_translation_check";
|
|
||||||
|
|
||||||
/* initialization */
|
|
||||||
{
|
|
||||||
const declarations = ts.createElementAccess(variable, ts.createLiteral(config.variables.declarations));
|
|
||||||
nodes.push(ts.createAssignment(declarations, ts.createBinary(declarations, SyntaxKind.BarBarToken, ts.createAssignment(declarations, ts.createObjectLiteral()))));
|
|
||||||
|
|
||||||
declarations_file = ts.createElementAccess(variable, ts.createLiteral(config.variables.declare_files));
|
|
||||||
nodes.push(ts.createAssignment(declarations_file, ts.createBinary(declarations_file, SyntaxKind.BarBarToken, ts.createAssignment(declarations_file, ts.createObjectLiteral()))));
|
|
||||||
|
|
||||||
variable = declarations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* test file already loaded */
|
|
||||||
{
|
|
||||||
const unique_id = sha256(source_file.fileName + " | " + (Date.now() / 1000));
|
|
||||||
const property = ts.createElementAccess(declarations_file, ts.createLiteral(unique_id));
|
|
||||||
|
|
||||||
const if_condition = ts.createBinary(property, SyntaxKind.ExclamationEqualsEqualsToken, ts.createIdentifier("undefined"));
|
|
||||||
//
|
|
||||||
let if_then: ts.Block;
|
|
||||||
{
|
|
||||||
const elements: ts.Statement[] = [];
|
|
||||||
|
|
||||||
const console = ts.createIdentifier("console.warn");
|
|
||||||
elements.push(ts.createCall(console, [], [ts.createLiteral("This file has already been loaded!\nAre you executing scripts twice?") as any]) as any);
|
|
||||||
elements.push(ts.createBreak(unique_check_label_name));
|
|
||||||
|
|
||||||
if_then = ts.createBlock(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
const if_else = ts.createAssignment(property, ts.createLiteral(unique_id));
|
|
||||||
blocked_nodes.push(ts.createIf(if_condition, if_then, if_else as any));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* test if variable has been defined somewhere else */
|
|
||||||
{
|
|
||||||
const for_variable_name = ts.createLoopVariable();
|
|
||||||
const for_variable_path = ts.createLoopVariable();
|
|
||||||
const for_declaration = ts.createVariableDeclarationList([ts.createVariableDeclaration(ts.createObjectBindingPattern([
|
|
||||||
ts.createBindingElement(undefined, config.optimized ? "n": "name", for_variable_name, undefined),
|
|
||||||
ts.createBindingElement(undefined, config.optimized ? "p": "path", for_variable_path, undefined)])
|
|
||||||
, undefined, undefined)]);
|
|
||||||
|
|
||||||
let for_block: ts.Statement;
|
|
||||||
{ //Create the for block
|
|
||||||
const elements: ts.Statement[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
const property = ts.createElementAccess(variable, for_variable_name);
|
|
||||||
const if_condition = ts.createBinary(property, SyntaxKind.ExclamationEqualsEqualsToken, ts.createIdentifier("undefined"));
|
|
||||||
|
|
||||||
//
|
|
||||||
const if_then = ts.createThrow(create_error(for_variable_name, for_variable_path, property));
|
|
||||||
const if_else = ts.createAssignment(property, for_variable_path);
|
|
||||||
const if_valid = ts.createIf(if_condition, if_then, if_else as any);
|
|
||||||
|
|
||||||
elements.push(if_valid);
|
|
||||||
|
|
||||||
for_block = ts.createBlock(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
let block = ts.createForOf(undefined,
|
|
||||||
for_declaration, ts.createArrayLiteral(
|
|
||||||
[...variables.map(e => ts.createObjectLiteral([
|
|
||||||
ts.createPropertyAssignment(config.optimized ? "n": "name", ts.createLiteral(e.name)),
|
|
||||||
ts.createPropertyAssignment(config.optimized ? "p": "path", ts.createLiteral(node_path(e.node)))
|
|
||||||
]))
|
|
||||||
])
|
|
||||||
, for_block);
|
|
||||||
block = ts.addSyntheticLeadingComment(block, SyntaxKind.MultiLineCommentTrivia, "Auto generated helper for testing if the translation keys are unique", true);
|
|
||||||
blocked_nodes.push(block);
|
|
||||||
}
|
|
||||||
return [...nodes, ts.createLabel(unique_check_label_name, ts.createBlock(blocked_nodes))];
|
|
||||||
}
|
|
||||||
|
|
||||||
let globalIdIndex = 0, globalIdTimestamp = Date.now();
|
|
||||||
export function transform(config: Configuration, context: ts.TransformationContext, source_file: ts.SourceFile) : TransformResult {
|
|
||||||
const cache: VolatileTransformConfig = {} as any;
|
|
||||||
cache.translations = [];
|
|
||||||
|
|
||||||
config.variables = (config.variables || {}) as any;
|
|
||||||
config.variables.base = config.variables.base || (config.optimized ? "__tr" : "_translations");
|
|
||||||
config.variables.declare_files = config.variables.declare_files || (config.optimized ? "f" : "declare_files");
|
|
||||||
config.variables.declarations = config.variables.declarations || (config.optimized ? "d" : "definitions");
|
|
||||||
|
|
||||||
//Initialize nodes
|
|
||||||
const extra_nodes: ts.Node[] = [];
|
|
||||||
{
|
|
||||||
cache.nodes = {} as any;
|
|
||||||
if(config.use_window) {
|
|
||||||
const window = ts.createIdentifier("window");
|
|
||||||
let translation_map = ts.createPropertyAccess(window, ts.createIdentifier(config.variables.base));
|
|
||||||
const new_translations = ts.createAssignment(translation_map, ts.createObjectLiteral());
|
|
||||||
|
|
||||||
let translation_map_init: ts.Expression = ts.createBinary(translation_map, ts.SyntaxKind.BarBarToken, new_translations);
|
|
||||||
translation_map_init = ts.createParen(translation_map_init);
|
|
||||||
|
|
||||||
extra_nodes.push(translation_map_init);
|
|
||||||
cache.nodes = {
|
|
||||||
translation_map: translation_map
|
|
||||||
};
|
|
||||||
} else if(config.module) {
|
|
||||||
cache.nodes = {
|
|
||||||
translation_map: ts.createIdentifier(config.variables.base)
|
|
||||||
};
|
|
||||||
|
|
||||||
extra_nodes.push(ts.createVariableDeclarationList([
|
|
||||||
ts.createVariableDeclaration(config.variables.base, undefined, ts.createObjectLiteral())
|
|
||||||
], ts.NodeFlags.Const), ts.createToken(SyntaxKind.SemicolonToken));
|
|
||||||
} else {
|
|
||||||
const variable_map = ts.createIdentifier(config.variables.base);
|
|
||||||
const inline_if = ts.createBinary(ts.createBinary(ts.createTypeOf(variable_map), SyntaxKind.ExclamationEqualsEqualsToken, ts.createLiteral("undefined")), ts.SyntaxKind.BarBarToken, ts.createAssignment(variable_map, ts.createObjectLiteral()));
|
|
||||||
|
|
||||||
cache.nodes = {
|
|
||||||
translation_map: variable_map,
|
|
||||||
};
|
|
||||||
|
|
||||||
extra_nodes.push(inline_if);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const generated_names: { name: string, node: ts.Node }[] = [];
|
|
||||||
let generator_base = 0;
|
|
||||||
|
|
||||||
const generate_unique_name = config => {
|
|
||||||
if(config.module) {
|
|
||||||
return "_" + generator_base++;
|
|
||||||
} else {
|
|
||||||
return "_" + globalIdTimestamp + "-" + ++globalIdIndex;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.name_generator = (config, node, message) => {
|
|
||||||
const name = generate_unique_name(config);
|
|
||||||
generated_names.push({name: name, node: node});
|
|
||||||
return name;
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.tsx_name_generator = generate_unique_name;
|
|
||||||
|
|
||||||
function visit(node: ts.Node): ts.Node {
|
|
||||||
node = ts.visitEachChild(node, visit, context);
|
|
||||||
return replace_processor(config, cache, node, source_file);
|
|
||||||
}
|
|
||||||
source_file = ts.visitNode(source_file, visit);
|
|
||||||
if(!config.module) {
|
|
||||||
/* we don't need a unique check because we're just in our scope */
|
|
||||||
extra_nodes.push(...generateUniqueCheck(config, source_file, cache.nodes.translation_map, generated_names));
|
|
||||||
}
|
|
||||||
|
|
||||||
source_file = ts.updateSourceFileNode(source_file, [...(extra_nodes as any[]), ...source_file.statements], source_file.isDeclarationFile, source_file.referencedFiles, source_file.typeReferenceDirectives, source_file.hasNoDefaultLib, source_file.referencedFiles);
|
|
||||||
|
|
||||||
const result: TransformResult = {} as any;
|
|
||||||
result.node = source_file;
|
|
||||||
result.translations = cache.translations;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generate_jsx_cache_key = (cache: VolatileTransformConfig, config: Configuration, element: ts.JsxElement) => ts.updateJsxElement(
|
|
||||||
element,
|
|
||||||
ts.updateJsxOpeningElement(
|
|
||||||
element.openingElement,
|
|
||||||
element.openingElement.tagName,
|
|
||||||
element.openingElement.typeArguments,
|
|
||||||
ts.updateJsxAttributes(element.openingElement.attributes, [
|
|
||||||
...element.openingElement.attributes.properties,
|
|
||||||
ts.createJsxAttribute(ts.createIdentifier("__cacheKey"), ts.createStringLiteral(cache.tsx_name_generator(config)))
|
|
||||||
])
|
|
||||||
),
|
|
||||||
element.children,
|
|
||||||
element.closingElement
|
|
||||||
);
|
|
||||||
|
|
||||||
export function replace_processor(config: Configuration, cache: VolatileTransformConfig, node: ts.Node, source_file: ts.SourceFile) : ts.Node {
|
|
||||||
if(config.verbose)
|
|
||||||
console.log("Process %s", SyntaxKind[node.kind]);
|
|
||||||
|
|
||||||
if(!node.getSourceFile())
|
|
||||||
return node;
|
|
||||||
|
|
||||||
if(ts.isCallExpression(node)) {
|
|
||||||
const call = <ts.CallExpression>node;
|
|
||||||
const call_name = call.expression["escapedText"] as string;
|
|
||||||
if(call_name != "tr") return node;
|
|
||||||
if(config.verbose) {
|
|
||||||
console.dir(call_name);
|
|
||||||
console.log("Parameters: %o", call.arguments.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(call.arguments.length > 1)
|
|
||||||
throw new Error(source_location(call) + ": tr(...) has been called with an invalid arguments (" + (call.arguments.length === 0 ? "too few" : "too many") + ")");
|
|
||||||
|
|
||||||
const fullText = call.getFullText(source_file);
|
|
||||||
if(fullText && fullText.indexOf("@tr-ignore") !== -1)
|
|
||||||
return node;
|
|
||||||
|
|
||||||
const object = <ts.StringLiteral>call.arguments[0];
|
|
||||||
if(object.kind != SyntaxKind.StringLiteral) {
|
|
||||||
if(call.getSourceFile())
|
|
||||||
throw new Error(source_location(call) + ": Ignoring tr call because given argument isn't of type string literal. (" + SyntaxKind[object.kind] + ")");
|
|
||||||
report(call, "Ignoring tr call because given argument isn't of type string literal. (" + SyntaxKind[object.kind] + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(config.verbose)
|
|
||||||
console.log("Message: %o", object.text || object.getText(source_file));
|
|
||||||
|
|
||||||
const variable_name = ts.createIdentifier(cache.name_generator(config, node, object.text || object.getText(source_file)));
|
|
||||||
const variable_init = ts.createPropertyAccess(cache.nodes.translation_map, variable_name);
|
|
||||||
|
|
||||||
const variable = ts.createPropertyAccess(cache.nodes.translation_map, variable_name);
|
|
||||||
const new_variable = ts.createAssignment(variable, call);
|
|
||||||
|
|
||||||
let { line, character } = source_file.getLineAndCharacterOfPosition(node.getStart());
|
|
||||||
|
|
||||||
cache.translations.push({
|
|
||||||
message: object.text || object.getText(source_file),
|
|
||||||
line: line,
|
|
||||||
character: character,
|
|
||||||
filename: (source_file || {fileName: "unknown"}).fileName,
|
|
||||||
type: "call"
|
|
||||||
});
|
|
||||||
|
|
||||||
return ts.createBinary(variable_init, ts.SyntaxKind.BarBarToken, new_variable);
|
|
||||||
} else if(node.kind === SyntaxKind.JsxElement) {
|
|
||||||
const element = node as ts.JsxElement;
|
|
||||||
const tag = element.openingElement.tagName as ts.Identifier;
|
|
||||||
|
|
||||||
if(tag.kind !== SyntaxKind.Identifier)
|
|
||||||
return node;
|
|
||||||
|
|
||||||
const properties = {} as any;
|
|
||||||
|
|
||||||
element.openingElement.attributes.properties.forEach((e: ts.JsxAttribute) => {
|
|
||||||
if(e.kind !== SyntaxKind.JsxAttribute)
|
|
||||||
throw new Error(source_location(e) + ": Invalid jsx attribute kind " + SyntaxKind[e.kind]);
|
|
||||||
|
|
||||||
if(e.name.kind !== SyntaxKind.Identifier)
|
|
||||||
throw new Error(source_location(e) + ": Key isn't an identifier");
|
|
||||||
|
|
||||||
properties[e.name.escapedText as string] = e.initializer;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(tag.escapedText === "Translatable") {
|
|
||||||
if('trIgnore' in properties && properties.trIgnore.kind === SyntaxKind.JsxExpression) {
|
|
||||||
const ignoreAttribute = properties.trIgnore as ts.JsxExpression;
|
|
||||||
if(ignoreAttribute.expression.kind === SyntaxKind.TrueKeyword)
|
|
||||||
return node;
|
|
||||||
else if(ignoreAttribute.expression.kind !== SyntaxKind.FalseKeyword)
|
|
||||||
throw new Error(source_location(ignoreAttribute) + ": Invalid attribute value of type " + SyntaxKind[ignoreAttribute.expression.kind]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(element.children.length < 1) {
|
|
||||||
throw new Error(source_location(element) + ": Element has been called with an invalid arguments (too few)");
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = element.children.map(element => {
|
|
||||||
if(element.kind === SyntaxKind.JsxText) {
|
|
||||||
return element.text;
|
|
||||||
} else if(element.kind === SyntaxKind.JsxSelfClosingElement) {
|
|
||||||
if(element.tagName.kind !== SyntaxKind.Identifier) {
|
|
||||||
throw new Error(source_location(element.tagName) + ": Expected a JsxSelfClosingElement, but received " + SyntaxKind[element.tagName.kind]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(element.tagName.escapedText !== "br") {
|
|
||||||
throw new Error(source_location(element.tagName) + ": Expected a br element, but received " + element.tagName.escapedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\n";
|
|
||||||
}
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
let { line, character } = source_file.getLineAndCharacterOfPosition(node.getStart());
|
|
||||||
cache.translations.push({
|
|
||||||
message: text,
|
|
||||||
line: line,
|
|
||||||
character: character,
|
|
||||||
filename: (source_file || {fileName: "unknown"}).fileName,
|
|
||||||
type: "jsx-translatable"
|
|
||||||
});
|
|
||||||
|
|
||||||
return generate_jsx_cache_key(cache, config, element);
|
|
||||||
} else if(tag.escapedText === "VariadicTranslatable") {
|
|
||||||
if(!('text' in properties))
|
|
||||||
throw new Error(source_location(element) + ": Missing text to translate");
|
|
||||||
|
|
||||||
const textAttribute = properties["text"] as ts.JsxExpression;
|
|
||||||
if(textAttribute.kind !== SyntaxKind.JsxExpression)
|
|
||||||
throw new Error(source_location(element) + ": Text attribute has an invalid type. Expected JsxExpression but received " + SyntaxKind[textAttribute.kind]);
|
|
||||||
|
|
||||||
if(textAttribute.expression.kind !== SyntaxKind.StringLiteral)
|
|
||||||
throw new Error(source_location(element) + ": Text attribute value isn't a string literal. Expected StringLiteral but received " + SyntaxKind[textAttribute.expression.kind]);
|
|
||||||
|
|
||||||
const literal = textAttribute.expression as ts.StringLiteral;
|
|
||||||
|
|
||||||
let { line, character } = source_file.getLineAndCharacterOfPosition(node.getStart());
|
|
||||||
cache.translations.push({
|
|
||||||
message: literal.text,
|
|
||||||
line: line,
|
|
||||||
character: character,
|
|
||||||
filename: (source_file || {fileName: "unknown"}).fileName,
|
|
||||||
type: "jsx-variadic-translatable"
|
|
||||||
});
|
|
||||||
|
|
||||||
return generate_jsx_cache_key(cache, config, element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
export interface Configuration {
|
|
||||||
use_window?: boolean;
|
|
||||||
replace_cache?: boolean;
|
|
||||||
verbose?: boolean;
|
|
||||||
|
|
||||||
optimized?: boolean;
|
|
||||||
module?: boolean;
|
|
||||||
|
|
||||||
variables?: {
|
|
||||||
base: string,
|
|
||||||
declarations: string,
|
|
||||||
declare_files: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransformResult {
|
|
||||||
node: ts.SourceFile;
|
|
||||||
translations: TranslationEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VolatileTransformConfig {
|
|
||||||
nodes: {
|
|
||||||
translation_map: ts.Expression;
|
|
||||||
};
|
|
||||||
|
|
||||||
name_generator: (config: Configuration, node: ts.Node, message: string) => string;
|
|
||||||
tsx_name_generator: (config: Configuration) => string;
|
|
||||||
translations: TranslationEntry[];
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
import * as ts from "typescript";
|
|
||||||
import * as ts_generator from "./ts_generator";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as mkdirp from "mkdirp";
|
|
||||||
|
|
||||||
import {writeFileSync} from "fs";
|
|
||||||
import {TranslationEntry} from "./generator";
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
target_file?: string;
|
|
||||||
verbose?: boolean;
|
|
||||||
optimized?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let process_config: Config;
|
|
||||||
export default function(program: ts.Program, config?: Config) : (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
|
|
||||||
process_config = config as any || {};
|
|
||||||
|
|
||||||
const base_path = path.dirname(program.getCompilerOptions().project || program.getCurrentDirectory());
|
|
||||||
if(process_config.verbose) {
|
|
||||||
console.log("TRGen transformer called");
|
|
||||||
console.log("Base path: %s", base_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('exit', function () {
|
|
||||||
if(!process_config.target_file) return;
|
|
||||||
|
|
||||||
const target = path.isAbsolute(process_config.target_file) ? process_config.target_file : path.join(base_path, process_config.target_file);
|
|
||||||
if(process_config.target_file) {
|
|
||||||
if(process_config.verbose) {
|
|
||||||
console.log("Writing translation file to " + target);
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirp.sync(path.dirname(target));
|
|
||||||
writeFileSync(target, JSON.stringify(translations, null, 2));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ctx => transformer(ctx) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
let processed = [];
|
|
||||||
const translations: TranslationEntry[] = [];
|
|
||||||
const transformer = (context: ts.TransformationContext) => (rootNode: ts.Node) => {
|
|
||||||
const handler = (rootNode: ts.Node) => {
|
|
||||||
if(rootNode.kind == ts.SyntaxKind.Bundle) {
|
|
||||||
const bundle = rootNode as ts.Bundle;
|
|
||||||
const result = [];
|
|
||||||
for(const file of bundle.sourceFiles)
|
|
||||||
result.push(handler(file));
|
|
||||||
return ts.updateBundle(bundle, result as any, bundle.prepends as any);
|
|
||||||
} else if(rootNode.kind == ts.SyntaxKind.SourceFile) {
|
|
||||||
const file = rootNode as ts.SourceFile;
|
|
||||||
|
|
||||||
if(processed.findIndex(e => e === file.fileName) !== -1) {
|
|
||||||
console.log("Skipping %s (already processed)", file.fileName);
|
|
||||||
return rootNode;
|
|
||||||
}
|
|
||||||
processed.push(file.fileName);
|
|
||||||
console.log("Processing " + file.fileName);
|
|
||||||
const result = ts_generator.transform({
|
|
||||||
use_window: false,
|
|
||||||
replace_cache: true,
|
|
||||||
module: true,
|
|
||||||
optimized: process_config.optimized
|
|
||||||
}, context, file);
|
|
||||||
translations.push(...result.translations);
|
|
||||||
return result.node;
|
|
||||||
} else {
|
|
||||||
console.warn("Invalid transform input: %s", ts.SyntaxKind[rootNode.kind]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return handler(rootNode);
|
|
||||||
};
|
|
|
@ -14,7 +14,8 @@
|
||||||
"generator.ts",
|
"generator.ts",
|
||||||
"index.ts",
|
"index.ts",
|
||||||
"compiler.ts",
|
"compiler.ts",
|
||||||
"jsrender_generator.ts",
|
"JsRendererGenerator.ts",
|
||||||
"ts_generator.ts"
|
"TsGenerator.ts",
|
||||||
|
"JsRendererTranslationLoader.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
import * as ts from "typescript";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import trtransformer from "./tools/trgen/ts_transformer";
|
|
||||||
import {exec} from "child_process";
|
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
|
||||||
import { DefinePlugin, Configuration } from "webpack";
|
import { DefinePlugin, Configuration } from "webpack";
|
||||||
|
|
||||||
|
@ -12,6 +10,7 @@ const ManifestGenerator = require("./webpack/ManifestPlugin");
|
||||||
const HtmlWebpackInlineSourcePlugin = require("./webpack/HtmlWebpackInlineSource");
|
const HtmlWebpackInlineSourcePlugin = require("./webpack/HtmlWebpackInlineSource");
|
||||||
|
|
||||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||||
|
import {TranslateableWebpackPlugin} from "./tools/trgen/WebpackPlugin";
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
@ -33,7 +32,7 @@ const generateDefinitions = async (target: string) => {
|
||||||
|
|
||||||
let timestamp;
|
let timestamp;
|
||||||
try {
|
try {
|
||||||
const { stdout } = await util.promisify(exec)("git show -s --format=%ct");
|
const { stdout } = await util.promisify(child_process.exec)("git show -s --format=%ct");
|
||||||
timestamp = parseInt(stdout.toString());
|
timestamp = parseInt(stdout.toString());
|
||||||
if(isNaN(timestamp)) {
|
if(isNaN(timestamp)) {
|
||||||
throw "failed to parse timestamp '" + stdout.toString() + "'";
|
throw "failed to parse timestamp '" + stdout.toString() + "'";
|
||||||
|
@ -83,189 +82,194 @@ const generateIndexPlugin = (target: "web" | "client"): HtmlWebpackPlugin => {
|
||||||
return new HtmlWebpackPlugin(options);
|
return new HtmlWebpackPlugin(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = async (target: "web" | "client"): Promise<Configuration & { devServer: any }> => ({
|
export const config = async (target: "web" | "client"): Promise<Configuration & { devServer: any }> => {
|
||||||
entry: {
|
const translateablePlugin = new TranslateableWebpackPlugin({ assetName: "translations.json" });
|
||||||
"loader": ["./loader/app/index.ts"],
|
|
||||||
"modal-external": ["./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts"],
|
|
||||||
//"devel-main": "./shared/js/devel_main.ts"
|
|
||||||
},
|
|
||||||
|
|
||||||
devtool: isDevelopment ? "inline-source-map" : "source-map",
|
return {
|
||||||
mode: isDevelopment ? "development" : "production",
|
entry: {
|
||||||
plugins: [
|
"loader": ["./loader/app/index.ts"],
|
||||||
new CleanWebpackPlugin(),
|
"modal-external": ["./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts"],
|
||||||
new DefinePlugin(await generateDefinitions(target)),
|
//"devel-main": "./shared/js/devel_main.ts"
|
||||||
|
},
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
devtool: isDevelopment ? "inline-source-map" : "source-map",
|
||||||
filename: isDevelopment ? "[name].[contenthash].css" : "[contenthash].css",
|
mode: isDevelopment ? "development" : "production",
|
||||||
chunkFilename: isDevelopment ? "[name].[contenthash].css" : "[contenthash].css",
|
plugins: [
|
||||||
ignoreOrder: true
|
new CleanWebpackPlugin(),
|
||||||
}),
|
new DefinePlugin(await generateDefinitions(target)),
|
||||||
|
|
||||||
new ManifestGenerator({
|
new MiniCssExtractPlugin({
|
||||||
outputFileName: "manifest.json",
|
filename: isDevelopment ? "[name].[contenthash].css" : "[contenthash].css",
|
||||||
context: __dirname
|
chunkFilename: isDevelopment ? "[name].[contenthash].css" : "[contenthash].css",
|
||||||
}),
|
ignoreOrder: true
|
||||||
|
}),
|
||||||
|
|
||||||
new SvgSpriteGenerator({
|
new ManifestGenerator({
|
||||||
dtsOutputFolder: path.join(__dirname, "shared", "svg-sprites"),
|
outputFileName: "manifest.json",
|
||||||
publicPath: "js/",
|
context: __dirname
|
||||||
configurations: {
|
}),
|
||||||
"client-icons": {
|
|
||||||
folder: path.join(__dirname, "shared", "img", "client-icons"),
|
new SvgSpriteGenerator({
|
||||||
cssClassPrefix: "client-",
|
dtsOutputFolder: path.join(__dirname, "shared", "svg-sprites"),
|
||||||
cssOptions: [
|
publicPath: "js/",
|
||||||
{
|
configurations: {
|
||||||
scale: 1,
|
"client-icons": {
|
||||||
selector: ".icon",
|
folder: path.join(__dirname, "shared", "img", "client-icons"),
|
||||||
unit: "px"
|
cssClassPrefix: "client-",
|
||||||
},
|
cssOptions: [
|
||||||
{
|
{
|
||||||
scale: 1.5,
|
scale: 1,
|
||||||
selector: ".icon_x24",
|
selector: ".icon",
|
||||||
unit: "px"
|
unit: "px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scale: 2,
|
scale: 1.5,
|
||||||
selector: ".icon_x32",
|
selector: ".icon_x24",
|
||||||
unit: "px"
|
unit: "px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scale: 1,
|
scale: 2,
|
||||||
selector: ".icon_em",
|
selector: ".icon_x32",
|
||||||
unit: "em"
|
unit: "px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 1,
|
||||||
|
selector: ".icon_em",
|
||||||
|
unit: "em"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dtsOptions: {
|
||||||
|
enumName: "ClientIcon",
|
||||||
|
classUnionName: "ClientIconClass",
|
||||||
|
module: false
|
||||||
}
|
}
|
||||||
],
|
|
||||||
dtsOptions: {
|
|
||||||
enumName: "ClientIcon",
|
|
||||||
classUnionName: "ClientIconClass",
|
|
||||||
module: false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
}),
|
|
||||||
|
|
||||||
generateIndexPlugin(target),
|
generateIndexPlugin(target),
|
||||||
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
|
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
|
||||||
|
|
||||||
//new BundleAnalyzerPlugin(),
|
translateablePlugin
|
||||||
].filter(e => !!e),
|
//new BundleAnalyzerPlugin(),
|
||||||
|
].filter(e => !!e),
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(s[ac]|c)ss$/,
|
test: /\.(s[ac]|c)ss$/,
|
||||||
use: [
|
use: [
|
||||||
//'style-loader',
|
//'style-loader',
|
||||||
{
|
{
|
||||||
loader: MiniCssExtractPlugin.loader,
|
loader: MiniCssExtractPlugin.loader,
|
||||||
options: {
|
options: {
|
||||||
esModule: false
|
esModule: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: {
|
||||||
|
mode: "local",
|
||||||
|
localIdentName: isDevelopment ? "[path][name]__[local]--[hash:base64:5]" : "[hash:base64]",
|
||||||
|
},
|
||||||
|
sourceMap: isDevelopment
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
sourceMap: isDevelopment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
loader: 'css-loader',
|
{
|
||||||
options: {
|
test: /\.tsx?$/,
|
||||||
modules: {
|
exclude: /node_modules/,
|
||||||
mode: "local",
|
|
||||||
localIdentName: isDevelopment ? "[path][name]__[local]--[hash:base64:5]" : "[hash:base64]",
|
|
||||||
},
|
|
||||||
sourceMap: isDevelopment
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'sass-loader',
|
|
||||||
options: {
|
|
||||||
sourceMap: isDevelopment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
|
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: "babel-loader",
|
loader: "babel-loader",
|
||||||
options: {
|
options: {
|
||||||
presets: ["@babel/preset-env"]
|
presets: ["@babel/preset-env"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: "ts-loader",
|
||||||
|
options: {
|
||||||
|
context: __dirname,
|
||||||
|
colors: true,
|
||||||
|
|
||||||
|
getCustomTransformers: program => ({
|
||||||
|
before: [translateablePlugin.createTypeScriptTransformer(program)]
|
||||||
|
}),
|
||||||
|
transpileOnly: isDevelopment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
loader: "ts-loader",
|
{
|
||||||
options: {
|
test: /\.was?t$/,
|
||||||
context: __dirname,
|
use: [
|
||||||
colors: true,
|
"./webpack/WatLoader.js"
|
||||||
getCustomTransformers: (prog: ts.Program) => {
|
]
|
||||||
return {
|
},
|
||||||
before: [trtransformer(prog, {
|
{
|
||||||
optimized: false,
|
test: /\.html$/i,
|
||||||
verbose: true,
|
use: [translateablePlugin.createTemplateLoader()],
|
||||||
target_file: path.join(__dirname, "dist", "translations.json")
|
type: "asset/source",
|
||||||
})]
|
},
|
||||||
};
|
{
|
||||||
},
|
test: /ChangeLog\.md$/i,
|
||||||
transpileOnly: isDevelopment
|
type: "asset/source",
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
]
|
test: /\.svg$/,
|
||||||
|
use: 'svg-inline-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|jpeg|gif)?$/,
|
||||||
|
type: "asset/resource"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} as any,
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js', ".scss"],
|
||||||
|
alias: {
|
||||||
|
"vendor/xbbcode": path.resolve(__dirname, "vendor/xbbcode/src"),
|
||||||
|
"tc-events": path.resolve(__dirname, "vendor/TeaEventBus/src/index.ts"),
|
||||||
|
"tc-services": path.resolve(__dirname, "vendor/TeaClientServices/src/index.ts"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.was?t$/,
|
|
||||||
use: [
|
|
||||||
"./webpack/WatLoader.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /ChangeLog\.md$|\.html$/i,
|
|
||||||
type: "asset/source",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: 'svg-inline-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(png|jpg|jpeg|gif)?$/,
|
|
||||||
type: "asset/resource"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} as any,
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.ts', '.js', ".scss"],
|
|
||||||
alias: {
|
|
||||||
"vendor/xbbcode": path.resolve(__dirname, "vendor/xbbcode/src"),
|
|
||||||
"tc-events": path.resolve(__dirname, "vendor/TeaEventBus/src/index.ts"),
|
|
||||||
"tc-services": path.resolve(__dirname, "vendor/TeaClientServices/src/index.ts"),
|
|
||||||
},
|
},
|
||||||
},
|
externals: [
|
||||||
externals: [
|
{"tc-loader": "window loader"}
|
||||||
{"tc-loader": "window loader"}
|
],
|
||||||
],
|
output: {
|
||||||
output: {
|
filename: isDevelopment ? "[name].[contenthash].js" : "[contenthash].js",
|
||||||
filename: isDevelopment ? "[name].[contenthash].js" : "[contenthash].js",
|
chunkFilename: isDevelopment ? "[name].[contenthash].js" : "[contenthash].js",
|
||||||
chunkFilename: isDevelopment ? "[name].[contenthash].js" : "[contenthash].js",
|
path: path.resolve(__dirname, 'dist'),
|
||||||
path: path.resolve(__dirname, 'dist'),
|
publicPath: "js/"
|
||||||
publicPath: "js/"
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
hints: false
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
chunks: "all",
|
|
||||||
maxSize: 512 * 1024
|
|
||||||
},
|
},
|
||||||
minimize: !isDevelopment,
|
performance: {
|
||||||
minimizer: [
|
hints: false
|
||||||
new TerserPlugin(),
|
},
|
||||||
new CssMinimizerPlugin()
|
optimization: {
|
||||||
]
|
splitChunks: {
|
||||||
},
|
chunks: "all",
|
||||||
devServer: {
|
maxSize: 512 * 1024
|
||||||
publicPath: "/",
|
},
|
||||||
contentBase: path.join(__dirname, 'dist'),
|
minimize: !isDevelopment,
|
||||||
writeToDisk: true,
|
minimizer: [
|
||||||
compress: true
|
new TerserPlugin(),
|
||||||
},
|
new CssMinimizerPlugin()
|
||||||
});
|
]
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
publicPath: "/",
|
||||||
|
contentBase: path.join(__dirname, 'dist'),
|
||||||
|
writeToDisk: true,
|
||||||
|
compress: true
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(compiler: webpack.Compiler) {
|
apply(compiler: webpack.Compiler) {
|
||||||
compiler.hooks.emit.tap(this.constructor.name, compilation => {
|
compiler.hooks.emit.tap("ManifestGenerator", compilation => {
|
||||||
const chunkData = {};
|
const chunkData = {};
|
||||||
for(const chunkGroup of compilation.chunkGroups) {
|
for(const chunkGroup of compilation.chunkGroups) {
|
||||||
const fileJs = [];
|
const fileJs = [];
|
||||||
|
|
Loading…
Add table
Reference in a new issue