#!/usr/bin/env node import { readFileSync, unlinkSync, writeFileSync } from 'fs'; import fs from 'fs' import ts from "typescript"; //Put the exports field back into package.json so that monorepos can work again let packageJson = readFileSync('package.json').toString(); packageJson = packageJson.slice(0, packageJson.lastIndexOf('}') - 1); //strip closing } packageJson += `, "exports": { ".": "./src/lib/index.ts", "./themes/*": "./src/lib/themes/*", "./styles/*": "./src/lib/styles/*", "./tailwind/*": "./src/lib/tailwind/*" } }`; writeFileSync('package.json', packageJson); unlinkSync('./package/.gitignore'); // delete .gitignore file from built package // Adding JSDoc comments to emitted .d.ts files from the package process // First read in JSDocced props from the svelte components at src/lib/components/ and src/lib/utilities/ into a mapping object // There is some brittleness in the assumption of how many spaces are at the beginning of a line on the emitted files // Also the definition files from svelte-package aren't exactly always clean either let filesToProps = {} function extractScriptsFromComponents(dir) { const leadingCharsToStrip = 8; //strip the src/lib/ from the filenames when adding it to the filesToProps mapping const list = fs.readdirSync(dir); list.forEach(file => { file = dir + '/' + file; const stat = fs.statSync(file); if (stat && stat.isDirectory()) { extractScriptsFromComponents(file); } else { if (file.endsWith('svelte')) { const src = readFileSync(file).toString(); // Split out the script block with lang='ts' section so that we can pass it to the TS compiler // there can be multiple script elements e.g. context='module' const begin = src.indexOf('"ts">\n') + 1; const script = src.substring(begin, src.indexOf('', begin)); // the first param is the filename, it is not for reading from the file, but rather for when ts reports issues // it is also not creating an actual source file, but rather an AST. const node = ts.createSourceFile(file.slice(leadingCharsToStrip), script, ts.ScriptTarget.Latest); filesToProps[file.slice(leadingCharsToStrip)] = { node: node }; } } }); } function extractJSDocBlocks() { for (const file in filesToProps) { let propsObj = {}; _extractJSDocBlocks(filesToProps[file].node, propsObj); filesToProps[file].props = propsObj; } } //Rescursive function for traversing node hierarchy to get JSDocs blocks, different node types have the information we want in different places function _extractJSDocBlocks(srcFile, propsObj) { ts.forEachChild(srcFile, node => { if (node?.jsDoc) { // console.log(srcFile); const jsDoc = node.jsDoc[node.jsDoc.length - 1] const declaration = node.declarationList?.declarations[0]; switch (node.kind) { case ts.SyntaxKind.FirstStatement: if (declaration.type?.typeName?.escapedText == 'CssClasses') { propsObj[declaration.name.escapedText] = { comment: jsDoc.comment, type: 'css' }; } else { propsObj[declaration.name.escapedText] = { comment: jsDoc.comment, type: 'prop' }; } break; case ts.SyntaxKind.ExpressionStatement: propsObj[node.expression.arguments[0].text] = { comment: jsDoc.tags[jsDoc.tags.length - 1].comment ?? '', type: 'event' }; break; } } _extractJSDocBlocks(node, propsObj) }) } function writeJSDocsToDefinitionFiles() { // these two will probably bite us in the ass later on.. but the maximum damage will be no descriptions in intellisense, can live with that. const propsBegin = 'props: {'; const eventsBegin = 'events: {'; const blockEnd = '}'; // we only insert JSDocs for properties that had a JSDoc block declared for them in the component file. Some props that might be defined // in the defintion file should not get a description as they are stores/context info derived from a parent component. for (let file in filesToProps) { let annotatedDts = []; const src = readFileSync('package/' + file + '.d.ts').toString().split('\n'); let inPropsSection = false; for (let line of src) { if (line.indexOf(blockEnd) != -1) { annotatedDts.push(line); inPropsSection = false; continue; } if (inPropsSection) { //there are a few that are not declared as optional, so we test for ? or : let endPos = line.indexOf('?'); if (endPos == -1) { endPos = line.indexOf(':'); } //Lookup the prop found in the definition file on our props mapping object //the 8 comes from the amount of spaces before the property begins, this is static and more efficient this way. const jsdoc = filesToProps[file].props[line.slice(8, endPos)]?.comment; if (jsdoc != undefined) { annotatedDts.push(' /** ' + jsdoc + '*/'); } } if (line.indexOf(propsBegin) != -1) { inPropsSection = true; } if (line.indexOf(eventsBegin) != -1) { inPropsSection = true; } annotatedDts.push(line); } writeFileSync('package/' + file + '.d.ts', annotatedDts.join('\n')); } } function generateKeyWordsFromProps() { let propSet = new Set(['class']) for (let file in filesToProps) { for (let prop in filesToProps[file].props) { if (filesToProps[file].props[prop].type == 'css') { propSet.add(prop) } } } writeFileSync('scripts/tw-settings.json', JSON.stringify({ "tailwindCSS.classAttributes": [...propSet] }, null, '\t')); } extractScriptsFromComponents('src/lib/components'); extractScriptsFromComponents('src/lib/utilities'); extractJSDocBlocks() writeJSDocsToDefinitionFiles(); generateKeyWordsFromProps()