@aurodesignsystem/auro-library 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ nodejs 20.12.1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Semantic Release Automated Changelog
2
2
 
3
+ # [2.9.0](https://github.com/AlaskaAirlines/auro-library/compare/v2.8.0...v2.9.0) (2024-10-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add matchWord to md magic config ([372cb28](https://github.com/AlaskaAirlines/auro-library/commit/372cb28a1c9eb0bd5f1014a4a8a9e6415cf659e5))
9
+
10
+
11
+ ### Features
12
+
13
+ * add handlebars template support ([bc3851d](https://github.com/AlaskaAirlines/auro-library/commit/bc3851dd87eb907927a3e2e22de53013c5e4e958))
14
+ * add new processing paradigm ([9a1dd25](https://github.com/AlaskaAirlines/auro-library/commit/9a1dd254e1288a6c7873b145a62087a37233a641))
15
+
16
+ # [2.8.0](https://github.com/AlaskaAirlines/auro-library/compare/v2.7.0...v2.8.0) (2024-09-19)
17
+
18
+
19
+ ### Features
20
+
21
+ * add runtime script for Floating UI [#65](https://github.com/AlaskaAirlines/auro-library/issues/65) ([e180fcb](https://github.com/AlaskaAirlines/auro-library/commit/e180fcb319e9ab6673765041b5a96057e562bd60))
22
+
3
23
  # [2.7.0](https://github.com/AlaskaAirlines/auro-library/compare/v2.6.3...v2.7.0) (2024-08-21)
4
24
 
5
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem/auro-library",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "This repository holds shared scripts, utilities, and workflows utilized across repositories along the Auro Design System.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -74,6 +74,7 @@
74
74
  "url": "https://github.com/AlaskaAirlines/auro-library/issues"
75
75
  },
76
76
  "dependencies": {
77
+ "handlebars": "^4.7.8",
77
78
  "markdown-magic": "^2.6.1",
78
79
  "npm-run-all": "^4.1.5"
79
80
  }
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import {Logger} from "../utils/logger.mjs";
4
+
5
+ export class AuroFileHandler {
6
+
7
+ /**
8
+ * Check if a file exists.
9
+ * @param {string} filePath - The file path to check.
10
+ * @returns {Promise<boolean>}
11
+ */
12
+ static async exists(filePath) {
13
+ try {
14
+ await fs.access(filePath);
15
+ return true;
16
+ // eslint-disable-next-line no-unused-vars
17
+ } catch (_err) {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Try to read a file and return its contents.
24
+ * @param {string} filePath - The file path to read.
25
+ * @returns {Promise<null|string>}
26
+ */
27
+ static async tryReadFile(filePath) {
28
+ try {
29
+ return await fs.readFile(filePath, {encoding: 'utf-8'});
30
+ } catch (err) {
31
+ Logger.error(`Error reading file: ${filePath}, ${err.message}`);
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Try to write a file with the given contents.
38
+ * @param {string} filePath - The file path to write to.
39
+ * @param {string} fileContents - The contents to write to the file.
40
+ * @returns {Promise<boolean>}
41
+ */
42
+ static async tryWriteFile(filePath, fileContents) {
43
+ try {
44
+ await fs.writeFile(filePath, fileContents, {encoding: 'utf-8'});
45
+ return true;
46
+ } catch (err) {
47
+ Logger.error(`Error writing file: ${filePath}, ${err.message}`);
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Try to copy a file from one location to another.
54
+ * @param {string} source - The source file path.
55
+ * @param {string} destination - The destination file path.
56
+ * @returns {Promise<boolean>}
57
+ */
58
+ static async tryCopyFile(source, destination) {
59
+ try {
60
+ await fs.copyFile(source, destination);
61
+ return true;
62
+ } catch (err) {
63
+ Logger.error(`Error copying file: ${source}, ${err.message}`);
64
+ return false;
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,169 @@
1
+ // eslint-disable
2
+ import Handlebars from "handlebars"
3
+ import fs from "node:fs/promises";
4
+
5
+ // declare package.json type with jsdoc
6
+ /**
7
+ * @typedef {Object} ExamplePackageJson
8
+ * @property {string} name - Name of the package.
9
+ * @property {string} version - Version of the package.
10
+ * @property {Record<string, string>} peerDependencies - Peer dependencies of the package.
11
+ */
12
+
13
+ // declare extracted names type with jsdoc
14
+ /**
15
+ * @typedef {Object} ExtractedNames
16
+ * @property {string} npm - NPM of the package.
17
+ * @property {string} namespace - Namespace of the package.
18
+ * @property {string} namespaceCap - Capitalized namespace of the package.
19
+ * @property {string} name - Name of the package.
20
+ * @property {string} nameCap - Capitalized name of the package.
21
+ * @property {string} version - Version of the package.
22
+ * @property {string} tokensVersion - Version of the design tokens.
23
+ * @property {string} wcssVersion - Version of the webcorestylesheets.
24
+ */
25
+
26
+
27
+ export class AuroTemplateFiller {
28
+ static designTokenPackage = '@aurodesignsystem/design-tokens';
29
+ static webCoreStylesheetsPackage = '@aurodesignsystem/webcorestylesheets';
30
+
31
+ constructor () {
32
+ /** @type {ExtractedNames} */
33
+ this.values = null;
34
+ }
35
+
36
+ async prepare() {
37
+ await this.extractNames()
38
+ }
39
+
40
+ /**
41
+ * Extract various data for filling template files from the package.json file.
42
+ * @returns {Promise<ExtractedNames>}
43
+ */
44
+ async extractNames() {
45
+ const packageJsonData = await fs.readFile('package.json', 'utf8');
46
+
47
+ /** @type {ExamplePackageJson} */
48
+ const parsedPackageJson = JSON.parse(packageJsonData);
49
+
50
+ const pName = parsedPackageJson.name;
51
+ const pVersion = parsedPackageJson.version;
52
+ const pdtVersion = parsedPackageJson.peerDependencies[AuroTemplateFiller.designTokenPackage].substring(1);
53
+ const wcssVersion = parsedPackageJson.peerDependencies[AuroTemplateFiller.webCoreStylesheetsPackage].substring(1);
54
+
55
+ const npmStart = pName.indexOf('@');
56
+ const namespaceStart = pName.indexOf('/');
57
+ const nameStart = pName.indexOf('-');
58
+
59
+ this.values = {
60
+ 'npm': pName.substring(npmStart, namespaceStart),
61
+ 'namespace': pName.substring(namespaceStart + 1, nameStart),
62
+ 'namespaceCap': pName.substring(namespaceStart + 1)[0].toUpperCase() + pName.substring(namespaceStart + 2, nameStart),
63
+ 'name': pName.substring(nameStart + 1),
64
+ 'nameCap': pName.substring(nameStart + 1)[0].toUpperCase() + pName.substring(nameStart + 2),
65
+ 'version': pVersion,
66
+ 'tokensVersion': pdtVersion,
67
+ wcssVersion
68
+ };
69
+ }
70
+
71
+ /**
72
+ * @param {string} template
73
+ * @param {ExtractedNames} values
74
+ * @return {string}
75
+ */
76
+ replaceTemplateValues(template) {
77
+ const compileResult = Handlebars.compile(template);
78
+
79
+ // replace all handlebars placeholders FIRST, then apply legacy replacements
80
+ let result = compileResult({
81
+ // TODO: consider replacing some of these with handlebars helpers
82
+ name: this.values.name,
83
+ Name: this.values.nameCap,
84
+ namespace: this.values.namespace,
85
+ Namespace: this.values.namespaceCap,
86
+ Version: this.values.version,
87
+ dtVersion: this.values.tokensVersion,
88
+ wcssVersion: this.values.wcssVersion
89
+ }, {
90
+ helpers: {
91
+ 'capitalize': (str) => str.charAt(0).toUpperCase() + str.slice(1),
92
+ 'withAuroNamespace': (str) => `auro-${str}`,
93
+ }
94
+ })
95
+
96
+ /**
97
+ * Old legacy template variables. We used to use `[varName]` and are now using handlebars `{{varName}}`.
98
+ * @type {[{pattern: RegExp, replacement: string},{pattern: RegExp, replacement: string},{pattern: RegExp, replacement: string},{pattern: RegExp, replacement: string},{pattern: RegExp, replacement: string},null,null,null]}
99
+ */
100
+ const legacyTemplateVariables = [
101
+ {
102
+ pattern: /\[npm\]/gu,
103
+ replacement: this.values.npm
104
+ },
105
+ {
106
+ pattern: /\[name\](?!\()/gu,
107
+ replacement: this.values.name
108
+ },
109
+ {
110
+ pattern: /\[Name\](?!\()/gu,
111
+ replacement: this.values.nameCap
112
+ },
113
+ {
114
+ pattern: /\[namespace\]/gu,
115
+ replacement: this.values.namespace
116
+ },
117
+ {
118
+ pattern: /\[Namespace\]/gu,
119
+ replacement: this.values.namespaceCap
120
+ },
121
+ {
122
+ pattern: /\[Version\]/gu,
123
+ replacement: this.values.version
124
+ },
125
+ {
126
+ pattern: /\[dtVersion\]/gu,
127
+ replacement: this.values.tokensVersion
128
+ },
129
+ {
130
+ pattern: /\[wcssVersion\]/gu,
131
+ replacement: this.values.wcssVersion
132
+ }
133
+ ];
134
+
135
+ /**
136
+ * Replace legacy placeholder strings.
137
+ */
138
+ for (const { pattern, replacement } of legacyTemplateVariables) {
139
+ result = result.replace(pattern, replacement);
140
+ }
141
+
142
+ /**
143
+ * Cleanup line breaks.
144
+ */
145
+ result = result.replace(/(\r\n|\r|\n)[\s]+(\r\n|\r|\n)/g, '\r\n\r\n'); // Replace lines containing only whitespace with a carriage return.
146
+ result = result.replace(/>(\r\n|\r|\n){2,}/g, '>\r\n'); // Remove empty lines directly after a closing html tag.
147
+ result = result.replace(/>(\r\n|\r|\n)```/g, '>\r\n\r\n```'); // Ensure an empty line before code samples.
148
+ result = result.replace(/>(\r\n|\r|\n){2,}```(\r\n|\r|\n)/g, '>\r\n```\r\n'); // Ensure no empty lines before close of code sample.
149
+ result = result.replace(/([^(\r\n|\r|\n)])(\r?\n|\r(?!\n))+#/g, "$1\r\n\r\n#"); // Ensure empty line before header sections.
150
+
151
+ return result;
152
+ }
153
+
154
+
155
+ /**
156
+ *
157
+ * @param {string} content
158
+ */
159
+ formatApiTable(content) {
160
+ let result = `${content}`;
161
+
162
+ result = result
163
+ .replace(/\r\n|\r|\n####\s`([a-zA-Z]*)`/g, `\r\n#### <a name="$1"></a>\`$1\`<a href="#" style="float: right; font-size: 1rem; font-weight: 100;">back to top</a>`)
164
+ .replace(/\r\n|\r|\n\|\s`([a-zA-Z]*)`/g, '\r\n| [$1](#$1)')
165
+ .replace(/\| \[\]\(#\)/g, "");
166
+
167
+ return result
168
+ }
169
+ }
@@ -1,229 +1,275 @@
1
1
  import path from 'path';
2
- import markdownMagic from 'markdown-magic';
3
- import fs from 'fs';
4
- import https from 'https';
2
+ import * as mdMagic from 'markdown-magic';
3
+ import fs from 'node:fs/promises';
5
4
  import { fileURLToPath } from 'url';
6
5
 
7
6
  import AuroLibraryUtils from "../utils/auroLibraryUtils.mjs";
7
+ import { AuroTemplateFiller } from "./auroTemplateFiller.mjs";
8
+ import { AuroFileHandler } from "./auroFileHandler.mjs";
9
+ import {Logger} from "../utils/logger.mjs";
8
10
 
9
- const auroLibraryUtils = new AuroLibraryUtils();
10
11
 
11
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
12
+ // This JSDoc type trickery is here so you get "decent enough" auto complete
13
+ /** @type {typeof import('markdown-magic').markdownMagic} */
14
+ const applyMarkdownMagic = mdMagic.default
12
15
 
13
- const dirDocTemplates = './docTemplates';
14
- const readmeFilePath = dirDocTemplates + '/README.md';
16
+ /**
17
+ * Optional output configuration
18
+ * @typedef {object} OutputConfig
19
+ * @property {string} [directory] - Change output path of new content. Default behavior is replacing the original file
20
+ * @property {boolean} [removeComments = false] - Remove comments from output. Default is false.
21
+ * @property {function} [pathFormatter] - Custom function for altering output paths
22
+ * @property {boolean} [applyTransformsToSource = false] - Apply transforms to source file. Default is true. This is for when outputDir is set.
23
+ */
15
24
 
16
- // List of components that do not support ESM to determine which README to use
17
- const nonEsmComponents = ['combobox', 'datepicker', 'menu', 'pane', 'select'];
25
+ /**
26
+ * Configuration for markdown magic
27
+ *
28
+ * Below is the main config for `markdown-magic` - copy-pasted directly from the library
29
+ *
30
+ * @typedef {object} MarkdownMagicOptions
31
+ * @property {FilePathsOrGlobs} [files] - Files to process.
32
+ * @property {Array} [transforms = defaultTransforms] - Custom commands to transform block contents, see transforms & custom transforms sections below.
33
+ * @property {OutputConfig} [output] - Output configuration
34
+ * @property {SyntaxType} [syntax = 'md'] - Syntax to parse
35
+ * @property {string} [open = 'doc-gen'] - Opening match word
36
+ * @property {string} [close = 'end-doc-gen'] - Closing match word. If not defined will be same as opening word.
37
+ * @property {string} [cwd = process.cwd() ] - Current working directory. Default process.cwd()
38
+ * @property {boolean} [outputFlatten] - Flatten files that are output
39
+ * @property {boolean} [useGitGlob] - Use git glob for LARGE file directories
40
+ * @property {boolean} [dryRun = false] - See planned execution of matched blocks
41
+ * @property {boolean} [debug = false] - See debug details
42
+ * @property {boolean} [silent = false] - Silence all console output
43
+ * @property {boolean} [applyTransformsToSource = true] - Apply transforms to source file. Default is true.
44
+ * @property {boolean} [failOnMissingTransforms = false] - Fail if transform functions are missing. Default skip blocks.
45
+ * @property {boolean} [failOnMissingRemote = true] - Fail if remote file is missing.
46
+ */
18
47
 
19
- function generateReadmeUrl() {
20
- let nameExtractionData = nameExtraction();
21
- let esmString = '';
22
48
 
23
- if (!nonEsmComponents.includes(nameExtractionData.name)) {
24
- esmString = '_esm';
49
+ // Config
50
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
51
+
52
+ /** @type {MarkdownMagicOptions} */
53
+ export const MD_MAGIC_CONFIG = {
54
+ matchWord: "AURO-GENERATED-CONTENT",
55
+ output: {
56
+ directory: "./",
57
+ applyTransformsToSource: true
25
58
  }
59
+ };
60
+
61
+ // Initialize utility services
62
+ export const auroLibraryUtils = new AuroLibraryUtils();
63
+ export const templateFiller = new AuroTemplateFiller();
64
+ export const auroFileHandler = new AuroFileHandler();
65
+
66
+ // List of components that do not support ESM to determine which README to use
67
+ export const nonEsmComponents = ['combobox', 'datepicker', 'menu', 'pane', 'select'];
26
68
 
27
- return 'https://raw.githubusercontent.com/AlaskaAirlines/WC-Generator/master/componentDocs/README' + esmString + '.md';
28
- }
29
69
 
70
+ // Local utils
30
71
  /**
31
- * Extract NPM, NAMESPACE and NAME from package.json
72
+ *
73
+ * @param {string} pathLike - Please include the preceding slash! Like so: `/docTemplates/README.md`
74
+ * @return {string}
32
75
  */
76
+ export function fromAuroComponentRoot(pathLike) {
77
+ const currentDir = fileURLToPath(new URL('.', import.meta.url))
78
+ return path.join(currentDir, `${auroLibraryUtils.projectRootFromBuildScriptDir}${pathLike}`)
79
+ }
33
80
 
34
- function nameExtraction() {
35
- let packageJson = fs.readFileSync('package.json', 'utf8', function(err, data) {
36
- if (err) {
37
- console.log('ERROR: Unable to read package.json file', err);
38
- }
39
- })
40
81
 
41
- packageJson = JSON.parse(packageJson);
42
-
43
- let pName = packageJson.name;
44
- let pVersion = packageJson.version;
45
- let pdtVersion = packageJson.peerDependencies['\@aurodesignsystem/design-tokens'].substring(1);
46
- let wcssVersion = packageJson.peerDependencies['\@aurodesignsystem/webcorestylesheets'].substring(1);
47
-
48
- let npmStart = pName.indexOf('@');
49
- let namespaceStart = pName.indexOf('/');
50
- let nameStart = pName.indexOf('-');
51
-
52
- return {
53
- 'npm': pName.substring(npmStart, namespaceStart),
54
- 'namespace': pName.substring(namespaceStart + 1, nameStart),
55
- 'namespaceCap': pName.substring(namespaceStart + 1)[0].toUpperCase() + pName.substring(namespaceStart + 2, nameStart),
56
- 'name': pName.substring(nameStart + 1),
57
- 'nameCap': pName.substring(nameStart + 1)[0].toUpperCase() + pName.substring(nameStart + 2),
58
- 'version': pVersion,
59
- 'tokensVersion': pdtVersion,
60
- 'wcssVersion': wcssVersion
61
- };
62
- }
82
+ // External assets
83
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
63
84
 
64
85
  /**
65
- * Replace all instances of [npm], [name], [Name], [namespace] and [Namespace] accordingly
86
+ * @param {string} tag - the release version tag to use instead of master
87
+ * @param {string} [variantOverride] - override the variant string
88
+ * @return {string}
66
89
  */
90
+ export function generateReadmeUrl(tag = 'master', variantOverride = '') {
91
+ // LEGACY CODE FOR NON-ESM COMPONENTS
67
92
 
68
- function formatTemplateFileContents(content, destination) {
69
- let nameExtractionData = nameExtraction();
70
- let result = content;
71
-
72
- /**
73
- * Replace placeholder strings
74
- */
75
- result = result.replace(/\[npm]/g, nameExtractionData.npm);
76
- result = result.replace(/\[name](?!\()/g, nameExtractionData.name);
77
- result = result.replace(/\[Name](?!\()/g, nameExtractionData.nameCap);
78
- result = result.replace(/\[namespace]/g, nameExtractionData.namespace);
79
- result = result.replace(/\[Namespace]/g, nameExtractionData.namespaceCap);
80
- result = result.replace(/\[Version]/g, nameExtractionData.version);
81
- result = result.replace(/\[dtVersion]/g, nameExtractionData.tokensVersion);
82
- result = result.replace(/\[wcssVersion]/g, nameExtractionData.wcssVersion);
83
-
84
- /**
85
- * Cleanup line breaks
86
- */
87
- result = result.replace(/(\r\n|\r|\n)[\s]+(\r\n|\r|\n)/g, '\r\n\r\n'); // Replace lines containing only whitespace with a carriage return.
88
- result = result.replace(/>(\r\n|\r|\n){2,}/g, '>\r\n'); // Remove empty lines directly after a closing html tag.
89
- result = result.replace(/>(\r\n|\r|\n)```/g, '>\r\n\r\n```'); // Ensure an empty line before code samples.
90
- result = result.replace(/>(\r\n|\r|\n){2,}```(\r\n|\r|\n)/g, '>\r\n```\r\n'); // Ensure no empty lines before close of code sample.
91
- result = result.replace(/([^(\r\n|\r|\n)])(\r?\n|\r(?!\n))+#/g, "$1\r\n\r\n#"); // Ensure empty line before header sections.
92
-
93
- /**
94
- * Write the result to the destination file
95
- */
96
- fs.writeFileSync(destination, result, { encoding: 'utf8'});
97
- }
93
+ const nameExtractionData = templateFiller.values;
94
+ let variantString = '';
98
95
 
99
- function formatApiTableContents(content, destination) {
100
- const nameExtractionData = nameExtraction();
101
- const wcName = nameExtractionData.namespace + '-' + nameExtractionData.name;
96
+ if (!nonEsmComponents.includes(nameExtractionData.name)) {
97
+ variantString = '_esm';
98
+ }
102
99
 
103
- let result = content;
100
+ // END LEGACY CODE
104
101
 
105
- result = result
106
- .replace(/\r\n|\r|\n####\s`([a-zA-Z]*)`/g, `\r\n#### <a name="$1"></a>\`$1\`<a href="#" style="float: right; font-size: 1rem; font-weight: 100;">back to top</a>`)
107
- .replace(/\r\n|\r|\n\|\s`([a-zA-Z]*)`/g, '\r\n| [$1](#$1)')
108
- .replace(/\| \[\]\(#\)/g, "");
102
+ if (variantOverride !== '') {
103
+ variantString = variantOverride;
104
+ }
109
105
 
110
- fs.writeFileSync(destination, result, { encoding: 'utf8'});
106
+ const baseRepoUrl = 'https://raw.githubusercontent.com/AlaskaAirlines/WC-Generator'
107
+ if (tag !== 'master') {
108
+ return `${baseRepoUrl}/refs/tags/${tag}/componentDocs/README` + variantString + '.md';
109
+ }
111
110
 
112
- fs.readFile('./demo/api.md', 'utf8', function(err, data) {
113
- formatTemplateFileContents(data, './demo/api.md');
114
- });
111
+ return `${baseRepoUrl}/master/componentDocs/README` + variantString + '.md';
115
112
  }
116
113
 
114
+ // Main Markdown magic processors
115
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
116
+
117
117
  /**
118
- * Compiles `./docTemplates/README.md` -> `./README.md`
118
+ * This is the expected object type when passing something other than a string.
119
+ * @typedef {Object} InputFileType
120
+ * @property {string} remoteUrl - The remote template to fetch
121
+ * @property {string} fileName - Path including file name to store
122
+ * @property {boolean} [overwrite] - Default is true. Choose to overwrite the file if it exists
119
123
  */
120
124
 
121
- function processReadme() {
122
- const callback = function(updatedContent, outputConfig) {
123
-
124
- if (fs.existsSync('./README.md')) {
125
- fs.readFile('./README.md', 'utf8', function(err, data) {
126
- formatTemplateFileContents(data, './README.md');
127
- });
128
- } else {
129
- console.log('ERROR: ./README.md file is missing');
130
- }
131
- };
132
125
 
133
- const config = {
134
- matchWord: 'AURO-GENERATED-CONTENT',
135
- outputDir: './'
136
- };
126
+ /**
127
+ * @typedef {Object} FileProcessorConfig
128
+ * @property {string | InputFileType} input - path to input file, including filename
129
+ * @property {string} output - path to output file, including filename
130
+ * @property {Partial<MarkdownMagicOptions>} [mdMagicConfig] - extra configuration options for md magic
131
+ * @property {Array<(contents: string) => string>} [postProcessors] - extra processor functions to run on content
132
+ */
137
133
 
138
- const markdownPath = path.join(__dirname, './../../../../../docTemplates/README.md');
139
134
 
140
- markdownMagic(markdownPath, config, callback);
141
- }
135
+ // Individual file processing steps
136
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
142
137
 
143
138
  /**
144
- * Compiles `./docTemplates/index.md` -> `./demo/index.md`
139
+ * Optionally retrieve a remote file using a provided configuration.
140
+ * @param {InputFileType} input - the input file configuration
141
+ * @return {Promise<void>}
145
142
  */
143
+ export async function optionallyRetrieveRemoteFile(input) {
144
+ const bareFileName = input.fileName
145
+ const shouldOverwrite = input.overwrite ?? true
146
146
 
147
- function processDemo() {
148
- const callback = function(updatedContent, outputConfig) {
149
- if (fs.existsSync('./demo/index.md')) {
150
- fs.readFile('./demo/index.md', 'utf8', function(err, data) {
151
- // formatTemplateFileContents(data, './demo/index.md');
152
- auroLibraryUtils.formatFileContents(data, './demo/index.md');
153
- });
154
- } else {
155
- console.log('ERROR: ./demo/index.md file is missing');
156
- }
157
- };
147
+ // If the file exists and overwrite is false, skip fetching
148
+ if (await AuroFileHandler.exists(input.fileName) && !shouldOverwrite) {
149
+ Logger.warn(`NOTICE: Using existing "${bareFileName}" file since overwrite is FALSE`);
150
+
151
+ return;
152
+ }
158
153
 
159
- const configDemo = {
160
- matchWord: 'AURO-GENERATED-CONTENT',
161
- outputDir: './demo'
162
- };
154
+ Logger.log(`Retrieving latest "${bareFileName}" file...`);
163
155
 
164
- const markdownPath = path.join(__dirname, './../../../../../docs/partials/index.md');
156
+ // 0b. Attempt to populate from remote file
157
+ const contents = await fetch(input.remoteUrl, {
158
+ redirect: 'follow'
159
+ }).then(r => r.text());
165
160
 
166
- markdownMagic(markdownPath, configDemo, callback);
161
+ // 0c. Write remote contents to local folder as cache
162
+ await AuroFileHandler.tryWriteFile(input.fileName, contents);
167
163
  }
168
164
 
165
+
169
166
  /**
170
- * Compiles `./docTemplates/api.md` -> `./demo/api.md`
167
+ * Run markdown magic on a file.
168
+ * @param {string} input
169
+ * @param {string} output
170
+ * @param {Partial<MarkdownMagicOptions>} [extraMdMagicConfig] - extra configuration options for md magic
171
+ * @return {Promise<void>}
171
172
  */
173
+ export async function runMarkdownMagicOnFile(input, output, extraMdMagicConfig = {}) {
174
+ await applyMarkdownMagic(output, {
175
+ ...MD_MAGIC_CONFIG,
176
+ ...extraMdMagicConfig
177
+ });
178
+ }
172
179
 
173
- function processApiExamples() {
174
- const callback = function(updatedContent, outputConfig) {
175
- if (fs.existsSync('./demo/api.md')) {
176
- fs.readFile('./demo/api.md', 'utf8', function(err, data) {
177
- formatApiTableContents(data, './demo/api.md');
178
- });
179
- } else {
180
- console.log('ERROR: ./demo/api.md file is missing');
181
- }
182
- };
183
-
184
- const config = {
185
- matchWord: 'AURO-GENERATED-CONTENT',
186
- outputDir: './demo'
187
- };
188
180
 
189
- const markdownPath = path.join(__dirname, './../../../../../docs/partials/api.md');
181
+ /**
182
+ * Process the content of a file.
183
+ *
184
+ * This is a high level function that performs the following via lower functions:
185
+ * - Read contents of file
186
+ * - Run "markdown-magic" on file contents (optional, *.md specific)
187
+ * - Run template variable replacement on file contents
188
+ * @param {FileProcessorConfig} config - the config for this file
189
+ */
190
+ export async function processContentForFile(config) {
191
+ const { input: rawInput, output, mdMagicConfig } = config
190
192
 
191
- markdownMagic(markdownPath, config, callback);
192
- }
193
+ // Helper vars
194
+ const derivedInputPath = typeof rawInput === 'string' ? rawInput : rawInput.fileName;
195
+ const segments = derivedInputPath.split("/")
196
+ const bareFileName = segments[segments.length - 1]
193
197
 
194
- /**
195
- * Copy README.md template from static source
196
- * */
198
+ // 0. Optionally retrieve a remote file
199
+ if (typeof rawInput === 'object') {
200
+ await optionallyRetrieveRemoteFile(rawInput);
201
+ }
197
202
 
198
- function copyReadmeLocally() {
199
- if (!fs.existsSync(dirDocTemplates)){
200
- fs.mkdirSync(dirDocTemplates);
203
+ // 1. Copy input or local input cache to output
204
+ if (!await AuroFileHandler.tryCopyFile(derivedInputPath, output)) {
205
+ throw new Error(`Error copying "${bareFileName}" file to output ${output}`);
201
206
  }
202
207
 
203
- if (!fs.existsSync(readmeFilePath)) {
204
- fs.writeFile(readmeFilePath, '', function(err) {
205
- if(err) {
206
- console.log('ERROR: Unable to create README.md file.', err);
207
- }
208
- });
208
+ // 2. If the file is a Markdown file, run markdown magic to inject contents and perform replacements
209
+ if (output.endsWith(".md")) {
210
+ await runMarkdownMagicOnFile(derivedInputPath, output, mdMagicConfig);
209
211
  }
210
212
 
211
- https.get(generateReadmeUrl(), function(response) {
212
- let writeTemplate = response.pipe(fs.createWriteStream(readmeFilePath));
213
+ // 3a. Read the output file contents
214
+ let fileContents = await fs.readFile(output, {encoding: 'utf-8'});
213
215
 
214
- writeTemplate.on('finish', () => {
215
- processReadme();
216
- });
216
+ // 3b. Replace template variables in output file
217
+ fileContents = templateFiller.replaceTemplateValues(fileContents);
217
218
 
218
- }).on('error', (err) => {
219
- console.log('ERROR: Unable to fetch README.md file from server.', err);
220
- });
219
+ // 3c. Run any post-processors
220
+ if (config.postProcessors) {
221
+ for (const processor of config.postProcessors) {
222
+ fileContents = processor(fileContents)
223
+ }
224
+ }
225
+
226
+ // 3d. Write the final file contents
227
+ if (!await AuroFileHandler.tryWriteFile(output, fileContents)) {
228
+ throw new Error(`Error writing "${bareFileName}" file to output ${output}`);
229
+ }
221
230
  }
222
231
 
232
+ // Finally, the main function
233
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
234
+
223
235
  /**
224
- * Run all the actual document generation
236
+ *
237
+ * @param {string} remoteReadmeVersion - the release version tag to use instead of master
238
+ * @param {string} [readmeVariant] - the release version tag to use instead of master
239
+ * @return {Promise<void>}
225
240
  */
226
- copyReadmeLocally();
227
- processApiExamples();
228
- processDemo();
241
+ export async function processDocFiles(remoteReadmeVersion = 'master', readmeVariant = undefined) {
242
+ // setup
243
+ await templateFiller.extractNames();
244
+
245
+ // process
246
+ // README.md
247
+
248
+ await processContentForFile({
249
+ input: {
250
+ remoteUrl: generateReadmeUrl(remoteReadmeVersion, readmeVariant),
251
+ fileName: fromAuroComponentRoot(`/docTemplates/README.md`),
252
+ },
253
+ output: fromAuroComponentRoot("/README.md")
254
+ })
229
255
 
256
+ // Demo MD file
257
+ await processContentForFile({
258
+ input: fromAuroComponentRoot("/docs/partials/index.md"),
259
+ output: fromAuroComponentRoot("/demo/index.md"),
260
+ mdMagicConfig: {
261
+ output: {
262
+ directory: fromAuroComponentRoot("/demo")
263
+ }
264
+ }
265
+ })
266
+
267
+ // API MD file
268
+ await processContentForFile({
269
+ input: fromAuroComponentRoot("/docs/partials/api.md"),
270
+ output: fromAuroComponentRoot("/demo/api.md"),
271
+ postProcessors: [
272
+ templateFiller.formatApiTable
273
+ ]
274
+ })
275
+ }
@@ -0,0 +1,141 @@
1
+ /* eslint-disable line-comment-position, no-inline-comments */
2
+
3
+ import {computePosition, offset, autoPlacement, flip} from '@floating-ui/dom';
4
+
5
+ export default class AuroFloatingUI {
6
+ bibUpdate(element) {
7
+ this.position(element, element.trigger, element.bib);
8
+ }
9
+
10
+ position(element, referenceEl, floatingEl) {
11
+ const middleware = [offset(element.floaterConfig.offset || 0)];
12
+
13
+ if (element.floaterConfig.flip) {
14
+ middleware.push(flip());
15
+ }
16
+
17
+ if (element.floaterConfig.autoPlacement) {
18
+ middleware.push(autoPlacement());
19
+ }
20
+
21
+ computePosition(referenceEl, floatingEl, {
22
+ placement: element.floaterConfig.placement || 'bottom',
23
+ middleware: middleware || []
24
+ }).then(({x, y}) => { // eslint-disable-line id-length
25
+ Object.assign(floatingEl.style, {
26
+ left: `${x}px`,
27
+ top: `${y}px`,
28
+ });
29
+ });
30
+ }
31
+
32
+ showBib(element) {
33
+ if (!element.disabled && !element.isPopoverVisible) {
34
+ // First, close any other dropdown that is already open
35
+ if (document.expandedAuroDropdown) {
36
+ // document.expandedAuroDropdown.hideBib();
37
+ this.hideBib(document.expandedAuroDropdown);
38
+ }
39
+
40
+ document.expandedAuroDropdown = this;
41
+
42
+ // Then, show this dropdown
43
+ element.bib.style.display = 'block';
44
+ this.bibUpdate(element);
45
+ element.isPopoverVisible = true; // does Floating UI already surface this?
46
+ element.trigger.setAttribute('aria-expanded', true);
47
+
48
+ // wrap this so we can clean it up when the bib is hidden
49
+ // document.querySelector('body').addEventListener('click', (evt) => {
50
+ // if (!evt.composedPath().includes(this)) {
51
+ // this.hideBib();
52
+ // }
53
+ // });
54
+ if (!element.noHideOnThisFocusLoss && !element.hasAttribute('noHideOnThisFocusLoss')) {
55
+ document.activeElement.addEventListener('focusout', () => {
56
+ if (document.activeElement !== document.querySelector('body') && !element.contains(document.activeElement)) {
57
+ this.hideBib(element);
58
+ }
59
+ });
60
+
61
+ document.querySelector('body').addEventListener('click', (evt) => {
62
+ if (!evt.composedPath().includes(element)) {
63
+ this.hideBib(element);
64
+ }
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ hideBib(element) {
71
+ if (element.isPopoverVisible && !element.disabled && !element.noToggle) { // do we really want noToggle here?
72
+ element.bib.style.display = ''; // should this be unset or none?
73
+ element.isPopoverVisible = false;
74
+ element.trigger.setAttribute('aria-expanded', false);
75
+ }
76
+ }
77
+
78
+ configure(element) {
79
+ element.trigger = element.shadowRoot.querySelector('#trigger');
80
+ element.bib = element.shadowRoot.querySelector('#bib');
81
+
82
+ element.trigger.addEventListener('click', (event) => this.handleEvent(event, element));
83
+ element.trigger.addEventListener('mouseenter', (event) => this.handleEvent(event, element));
84
+ element.trigger.addEventListener('mouseleave', (event) => this.handleEvent(event, element));
85
+ element.trigger.addEventListener('focus', (event) => this.handleEvent(event, element));
86
+ element.trigger.addEventListener('blur', (event) => this.handleEvent(event, element));
87
+ }
88
+
89
+ handleClick(element) {
90
+ if (this.isPopoverVisible) {
91
+ this.hideBib(element);
92
+ } else {
93
+ this.showBib(element);
94
+ }
95
+
96
+ // should this be left in dropdown?
97
+ const event = new CustomEvent('auroDropdown-triggerClick', {
98
+ composed: true,
99
+ details: {
100
+ expanded: this.isPopoverVisible
101
+ }
102
+ });
103
+
104
+ element.dispatchEvent(event);
105
+ }
106
+
107
+ handleEvent(event, element) {
108
+ if (!element.disableEventShow) {
109
+ switch (event.type) {
110
+ case 'mouseenter':
111
+ if (element.hoverToggle) {
112
+ this.showBib(element);
113
+ }
114
+ break;
115
+ case 'mouseleave':
116
+ if (element.hoverToggle) {
117
+ this.hideBib(element);
118
+ }
119
+ break;
120
+ case 'focus':
121
+ if (element.focusShow) {
122
+ // this needs to better handle clicking that gives focus - currently it shows and then immediately hides the bib
123
+ this.showBib(element);
124
+ }
125
+ break;
126
+ case 'blur':
127
+ // this likely needs to be improved to handle focus within the bib for datepicker
128
+ if (!element.noHideOnThisFocusLoss && !element.hasAttribute('noHideOnThisFocusLoss')) { // why do we have to do both here?
129
+ this.hideBib(element);
130
+ }
131
+ break;
132
+ case 'click':
133
+ this.handleClick(element);
134
+ break;
135
+ default:
136
+ // do nothing
137
+ // add cases for show and toggle by keyboard space and enter key - maybe this is handled already?
138
+ }
139
+ }
140
+ }
141
+ }
@@ -6,10 +6,16 @@
6
6
  /* eslint-disable arrow-parens, line-comment-position, no-console, no-inline-comments, no-magic-numbers, prefer-arrow-callback, require-unicode-regexp, jsdoc/require-description-complete-sentence, prefer-named-capture-group */
7
7
 
8
8
  import * as fs from 'fs';
9
+ import fsAsync from 'node:fs/promises';
9
10
  import * as path from 'path';
10
11
  import chalk from 'chalk';
11
12
 
12
13
  export default class AuroLibraryUtils {
14
+ PROJECT_ROOT_FROM_SCRIPTS__BUILD = "./../../../../.."
15
+
16
+ get projectRootFromBuildScriptDir() {
17
+ return this.PROJECT_ROOT_FROM_SCRIPTS__BUILD
18
+ }
13
19
 
14
20
  /**
15
21
  * Copies and pastes all files in a source directory into a destination directory.
@@ -73,7 +79,7 @@ export default class AuroLibraryUtils {
73
79
  /**
74
80
  * Logs out messages in a readable format.
75
81
  * @param {String} message - Message to be logged.
76
- * @param {String} status - Status that determines the color of the logged message.
82
+ * @param {"info" | "success" | "error"} status - Status that determines the color of the logged message.
77
83
  * @param {Boolean} section - If true, adds a box around the message for readability.
78
84
  */
79
85
  auroLogger(message, status, section) {
@@ -185,5 +191,19 @@ export default class AuroLibraryUtils {
185
191
  */
186
192
  fs.writeFileSync(destination, result, { encoding: 'utf8'});
187
193
  }
194
+
195
+ /**
196
+ * Check if a file or directory exists.
197
+ * @param filePath
198
+ * @return {Promise<boolean>}
199
+ */
200
+ async existsAsync(filePath) {
201
+ try {
202
+ await fsAsync.access(filePath);
203
+ return true;
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
188
208
  }
189
209
 
@@ -0,0 +1,73 @@
1
+ /* eslint-disable no-inline-comments, no-console, line-comment-position */
2
+
3
+ import chalk from 'chalk';
4
+
5
+ export class Logger {
6
+
7
+ /**
8
+ * Logs out messages in a readable format.
9
+ * @param {String} message - Message to be logged.
10
+ * @param {false | "info" | "success" | "error" | "warn"} status - Status that determines the color of the logged message.
11
+ * @param {Boolean} section - If true, adds a box around the message for readability.
12
+ */
13
+ static auroLogger(message, status, section) {
14
+ if (status !== false) {
15
+ const infoColor = '#0096FF'; // blue
16
+ const successColor = '#4CBB17'; // green
17
+ const errorColor = '#ff0000'; // red
18
+ const warningColor = '#FFA500'; // orange
19
+
20
+ let color = undefined; // eslint-disable-line no-undef-init
21
+
22
+ if (status === 'info') {
23
+ color = infoColor;
24
+ } else if (status === 'success') {
25
+ color = successColor;
26
+ } else if (status === 'error') {
27
+ color = errorColor;
28
+ } else if (status === 'warn') {
29
+ color = warningColor;
30
+ }
31
+
32
+ if (section) {
33
+ console.log(chalk.hex(color)(`╭ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ──────────────────────────────╮\n`));
34
+ }
35
+
36
+ console.log(chalk.hex(color)(message));
37
+
38
+ if (section) {
39
+ console.log(chalk.hex(color)('\n╰─────────────────────────────── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯'));
40
+ }
41
+ } else {
42
+ if (section) {
43
+ console.log(`╭ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ──────────────────────────────╮\n`);
44
+ }
45
+
46
+ console.log(message);
47
+
48
+ if (section) {
49
+ console.log(`\n╰─────────────────────────────── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯`);
50
+ }
51
+ }
52
+ }
53
+
54
+ static log(message, section = false) {
55
+ Logger.auroLogger(message, false, section);
56
+ }
57
+
58
+ static info(message, section = false) {
59
+ Logger.auroLogger(message, "info", section);
60
+ }
61
+
62
+ static warn(message, section = false) {
63
+ Logger.auroLogger(message, "warn", section);
64
+ }
65
+
66
+ static success(message, section = false) {
67
+ Logger.auroLogger(message, "success", section);
68
+ }
69
+
70
+ static error(message, section = false) {
71
+ Logger.auroLogger(message, "error", section);
72
+ }
73
+ }