Skip to content

Commit

Permalink
Tidy up sync source.
Browse files Browse the repository at this point in the history
  • Loading branch information
SilasBerger committed Jan 3, 2024
1 parent 496336e commit a6e5dca
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 237 deletions.
4 changes: 2 additions & 2 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {themes as prismThemes} from 'prism-react-renderer';
import type {Config, LoadContext, PluginOptions} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
import {loadSiteConfig} from "./src/builder/site-loader";
import {loadSiteConfig} from "./src/builder/site-config-loader";
import {buildScripts} from "./src/builder/scripts-builder";
import {SCRIPTS_ROOT} from "./config/builder-config";
import * as osPath from "path";
import { Logger } from './src/builder/logger';
import { Logger } from './src/builder/util/logger';

const siteConfig = loadSiteConfig();
Logger.instance.info(`🔧 Building site '${siteConfig.siteId}'`);
Expand Down
25 changes: 25 additions & 0 deletions src/builder/models/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {DestNode, SourceNode} from "../sync/sync-nodes";

export type SyncPair = [SourceNode, DestNode]

export enum SourceCandidateType {
MAPPED,
MARKED,
}

export interface MappedSourceCandidate {
type: SourceCandidateType.MAPPED;
implicit?: boolean;
node: SourceNode;
}

export interface MarkedSourceCandidate {
type: SourceCandidateType.MARKED;
implicit?: boolean;
node: SourceNode;
markerSpecificity: number;
}

export type SourceCandidate = MappedSourceCandidate | MarkedSourceCandidate;

export type SourceCandidateGenerator = (sourceNode: SourceNode) => SourceCandidate
38 changes: 24 additions & 14 deletions src/builder/scripts-builder.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
import * as fs from "fs";
import {ScriptConfig } from "./models/script-config";
import {createDestTree, createSourceTree, SourceNode} from "./sync/sync-tree";
import {DestNode, SourceNode} from "./sync/sync-nodes";
import * as osPath from "path";
import {MATERIAL_ROOT, SCRIPTS_ROOT} from "../../config/builder-config";
import {synchronizeToScript} from "./sync/sync";
import {Logger} from "./logger";
import {Logger} from "./util/logger";
import {createDestTree, createSourceTree} from "./sync/sync-tree-builder";
import {copyFilesToScriptDir, removeObsoleteScriptFiles} from "./sync/file-ops";
import {applyMarkers, applySectionMappings, collectSyncPairs} from "./sync/sync-tree-processing";

export function buildScripts(scriptsConfigsFile: string) {
const scriptsConfigs = loadScriptsConfigs(scriptsConfigsFile);
const scriptsConfigs = _loadScriptsConfigs(scriptsConfigsFile);

const materialTree = createMaterialTree();
const materialTree = _createMaterialTree();
Object.entries(scriptsConfigs).forEach(([scriptRoot, scriptConfig]: [string, ScriptConfig]) => {
buildScript(scriptRoot, scriptConfig, materialTree);
_buildScript(scriptRoot, scriptConfig, materialTree);
});
return Object.keys(scriptsConfigs);
}

function createMaterialTree(): SourceNode {
const materialRootPath = osPath.resolve(MATERIAL_ROOT);
return createSourceTree(materialRootPath);
}

function loadScriptsConfigs(scriptsConfigsName: string) {
function _loadScriptsConfigs(scriptsConfigsName: string) {
const scriptsConfigsPath = `config/scriptsConfigs/${scriptsConfigsName}`;
if (!fs.existsSync(scriptsConfigsPath)) {
throw `No such scriptsConfigs file: ${scriptsConfigsPath}`;
}
return JSON.parse(fs.readFileSync(scriptsConfigsPath).toString());
}

function buildScript(scriptRoot: string, scriptConfig: ScriptConfig, materialTree: SourceNode) {
function _createMaterialTree(): SourceNode {
const materialRootPath = osPath.resolve(MATERIAL_ROOT);
return createSourceTree(materialRootPath);
}

function _buildScript(scriptRoot: string, scriptConfig: ScriptConfig, materialTree: SourceNode) {
Logger.instance.info(`📝 Building script '${scriptRoot}'`);
const scriptRootPath = osPath.resolve(osPath.join(SCRIPTS_ROOT, scriptRoot));
const scriptTree = createDestTree(scriptRootPath);
synchronizeToScript(materialTree, scriptTree, scriptConfig);
_synchronizeToScript(materialTree, scriptTree, scriptConfig);
}

function _synchronizeToScript(materialTree: SourceNode, scriptTree: DestNode, scriptConfig: ScriptConfig): void {
applySectionMappings(scriptConfig, scriptTree, materialTree);
applyMarkers(materialTree, scriptTree, scriptConfig.markers);
const syncPairs = collectSyncPairs(scriptTree);
copyFilesToScriptDir(syncPairs);
removeObsoleteScriptFiles(scriptTree);
}
File renamed without changes.
53 changes: 53 additions & 0 deletions src/builder/sync/file-ops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {DestNode, SyncNode} from "./sync-nodes";
import {Logger} from "../util/logger";
import osPath from "path";
import * as fs from "fs-extra";

export function copyFilesToScriptDir(syncPairs: [SyncNode, SyncNode][]): void {
Logger.instance.info('🖨 Copying resources to script...')
syncPairs.forEach(([sourceNode, destNode]) => {
copyFileIfChanged(sourceNode.absPath, destNode.absPath);
})
}

function copyFileIfChanged(sourcePath: string, destPath: string): void {
if (!fileHasChanged(sourcePath, destPath)) {
return;
}

const destDir = osPath.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}

Logger.instance.info(`[COPY] '${sourcePath}' -> '${destPath}'`);
fs.copySync(sourcePath, destPath, { preserveTimestamps: true });
}

function fileHasChanged(sourcePath: string, destPath: string): boolean {
if (!fs.existsSync(destPath)) {
return true;
}

const sourceStats = fs.statSync(sourcePath);
const destStats = fs.statSync(destPath);
return sourceStats.mtime.toUTCString() != destStats.mtime.toUTCString();
}

export function removeObsoleteScriptFiles(scriptTree: DestNode): void {
const deletionCandidates = scriptTree
.collect((node: DestNode) => !node.hasUsableSourceCandidates());
if (deletionCandidates.length > 0) {
Logger.instance.info('🗑️ Deleting obsolete script files...');
deletionCandidates.forEach(candidate => {
deleteFile(candidate.absPath);
});
}
}

function deleteFile(path: string): void {
if (fs.existsSync(path)) {
Logger.instance.info(`[DELETE] '${path}'`);
fs.rmSync(path, {recursive: true})
}
}
2 changes: 1 addition & 1 deletion src/builder/sync/marker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {calculateSpecificity, canonicalNameFrom, hasApplicableMarkers, markersFrom} from "./markers";
import {SourceNode} from "../sync/sync-tree";
import {SourceNode} from "./sync-nodes";
import {MarkersDefinition} from "../models/script-config";

describe('markersFrom', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/builder/sync/markers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {MarkersDefinition} from "../models/script-config";
import {MarkerWithSpecificity} from "../models/markers";
import {SourceNode} from "./sync-tree";
import {SourceNode} from "./sync-nodes";
import _ from "lodash";
import {Optional} from "../../shared/util/optional";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {DestNode, MappedSourceCandidate, SourceCandidateType, SourceNode} from "./sync-tree";
import {DestNode, SourceNode} from "./sync-nodes";
import {MappedSourceCandidate, SourceCandidateType} from "../models/sync";

describe('SyncNode', () => {
describe('absPath', () => {
Expand Down Expand Up @@ -244,5 +245,4 @@ describe('SourceNode', () => {
/*
TODO:
- DestNode
- sync-tree -> functions
*/
54 changes: 2 additions & 52 deletions src/builder/sync/sync-tree.ts → src/builder/sync/sync-nodes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as fs from "fs";
import * as osPath from "path";
import {Optional} from "../../shared/util/optional";
import {canonicalNameFrom, markersFrom} from "../../builder/sync/markers";
import { Logger } from "../logger";
import {Logger} from "../util/logger";
import {SourceCandidate, SourceCandidateGenerator} from "../models/sync";

export abstract class SyncNode {

Expand Down Expand Up @@ -201,53 +201,3 @@ export class DestNode extends SyncNode {
.filter(candidate => !candidate.node.isIgnored).length > 0;
}
}

// TODO: Consider moving these things to a different file.
export enum SourceCandidateType {
MAPPED,
MARKED,
}

export interface MappedSourceCandidate {
type: SourceCandidateType.MAPPED;
implicit?: boolean;
node: SourceNode;
}

export interface MarkedSourceCandidate {
type: SourceCandidateType.MARKED;
implicit?: boolean;
node: SourceNode;
markerSpecificity: number;
}

export type SourceCandidate = MappedSourceCandidate | MarkedSourceCandidate;

export type SourceCandidateGenerator = (sourceNode: SourceNode) => SourceCandidate

export function createSourceTree(rootPath: string): SourceNode {
const sourceRoot = new SourceNode(rootPath, []);
_createDirTree(sourceRoot, rootPath);
return sourceRoot;
}

export function createDestTree(rootPath: string): DestNode {
const destRoot = new DestNode(rootPath);
_createDirTree(destRoot, rootPath);
return destRoot;
}

function _createDirTree(currentNode: SyncNode, currentAbsPath: string): void {
if (!fs.existsSync(currentAbsPath)) {
return;
}
fs.readdirSync(currentAbsPath).forEach(childPath => {
const childAbsPath = osPath.join(currentAbsPath, childPath);
if (fs.statSync(childAbsPath).isFile()) {
currentNode.appendChild(childPath);
} else {
const childNode = currentNode.appendChild(childPath);
_createDirTree(childNode, childAbsPath);
}
});
}
30 changes: 30 additions & 0 deletions src/builder/sync/sync-tree-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import fs from "fs";
import osPath from "path";
import {DestNode, SourceNode, SyncNode} from "./sync-nodes";

export function createSourceTree(rootPath: string): SourceNode {
const sourceRoot = new SourceNode(rootPath, []);
_createDirTree(sourceRoot, rootPath);
return sourceRoot;
}

export function createDestTree(rootPath: string): DestNode {
const destRoot = new DestNode(rootPath);
_createDirTree(destRoot, rootPath);
return destRoot;
}

function _createDirTree(currentNode: SyncNode, currentAbsPath: string): void {
if (!fs.existsSync(currentAbsPath)) {
return;
}
fs.readdirSync(currentAbsPath).forEach(childPath => {
const childAbsPath = osPath.join(currentAbsPath, childPath);
if (fs.statSync(childAbsPath).isFile()) {
currentNode.appendChild(childPath);
} else {
const childNode = currentNode.appendChild(childPath);
_createDirTree(childNode, childAbsPath);
}
});
}
107 changes: 107 additions & 0 deletions src/builder/sync/sync-tree-processing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {DestNode, SourceNode} from "./sync-nodes";
import {MarkersDefinition, ScriptConfig, SectionMapping} from "../models/script-config";
import * as osPath from 'path';
import {calculateSpecificity, canonicalNameFrom, hasApplicableMarkers} from "./markers";
import {MarkedSourceCandidate, SourceCandidate, SourceCandidateType, SyncPair} from "../models/sync";

export function applySectionMappings(scriptConfig: ScriptConfig, scriptTree: DestNode, materialTree: SourceNode): void {
scriptConfig.mappings.forEach(sectionMapping => {
const sourceNode = _applySectionMapping(sectionMapping, scriptTree, materialTree);
_applyIgnorePaths(sectionMapping, sourceNode);
});
}

function _applySectionMapping(sectionMapping: SectionMapping, scriptTree: DestNode, materialTree: SourceNode): SourceNode {
const sourceSegments = _splitPathSegments(sectionMapping.material);
const destSegments = _splitPathSegments(sectionMapping.section);

const sourceNode = materialTree
.findNode(sourceSegments)
.expect(`Material tree does not have a node '${sectionMapping.material}'`) as SourceNode;
const destNode = scriptTree.ensureNode(destSegments);

sourceNode.propagateAsSourceCandidateFor(destNode, (node => {
return {
type: SourceCandidateType.MAPPED,
node,
}
}));

return sourceNode;
}

function _applyIgnorePaths(sectionMapping: SectionMapping, sourceNode: SourceNode) {
if (!sectionMapping.ignore) {
return;
}

sectionMapping.ignore.forEach(ignorePath => {
sourceNode
.findNode(_splitPathSegments(ignorePath))
.ifPresent((ignoredRootNode: SourceNode) => {
ignoredRootNode.propagateAsIgnored();
});
});
}

export function applyMarkers(sourceTree: SourceNode, destTree: DestNode, markersDefinition: MarkersDefinition) {
sourceTree
.collect((sourceNode: SourceNode) => sourceNode.isMarked)
.filter((markedNode: SourceNode) => hasApplicableMarkers(markedNode, markersDefinition))
.forEach((markedNode: SourceNode) => {
const specificity = calculateSpecificity(markedNode, markersDefinition);
const canonicalPathSegments = _splitPathSegments(markedNode.treePath)
.map(segment => canonicalNameFrom(segment));
const destNode = destTree.ensureNode(canonicalPathSegments);

markedNode.propagateAsSourceCandidateFor(destNode, (sourceNode: SourceNode) => {
return {
type: SourceCandidateType.MARKED,
node: sourceNode,
markerSpecificity: specificity
}
});
});
}

export function collectSyncPairs(processedScriptTree: DestNode): SyncPair[] {
return (processedScriptTree
.collect(node => node.isLeaf()) as DestNode[])
.filter(leaf => leaf.hasUsableSourceCandidates())
.map(destNode => {
return [_determineBestSourceCandidate(destNode.sourceCandidates), destNode];
});
}

function _determineBestSourceCandidate(candidates: SourceCandidate[]): SourceNode {
const sortMarkedCandidatesBySpecificity = (a: MarkedSourceCandidate, b: MarkedSourceCandidate) => {
return a.markerSpecificity - b.markerSpecificity;
};

const mapped = candidates
.filter(candidate => candidate.type == SourceCandidateType.MAPPED);
const mappedExplicit = mapped.filter(candidate => !candidate.implicit);
const mappedImplicit = mapped.filter(candidate => candidate.implicit);

const marked = candidates
.filter(candidate => candidate.type == SourceCandidateType.MARKED);
const markedExplicit = marked
.filter(candidate => !candidate.implicit)
.sort(sortMarkedCandidatesBySpecificity)
const markedImplicit = marked
.filter(candidate => candidate.implicit)
.sort(sortMarkedCandidatesBySpecificity)

const sortedCandidates = [
...mappedExplicit,
...markedExplicit,
...mappedImplicit,
...markedImplicit
];

return sortedCandidates[0].node;
}

function _splitPathSegments(path: string): string[] {
return path.split(osPath.sep).filter(segment => !!segment && segment != '.');
}
Loading

0 comments on commit a6e5dca

Please sign in to comment.