diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..01fa7c9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +* text eol=lf +*.exe binary +*.png binary +*.jpg binary +*.jpeg binary +*.ico binary +*.icns binary +*.eot binary +*.otf binary +*.ttf binary +*.woff binary +*.woff2 binary +*.ldml binary +*.zip binary diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..5a941a5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,88 @@ +name: 'CodeQL' + +on: + push: + branches: ['main', 'release-prep', 'hotfix-*'] + pull_request: + branches: ['main', 'release-prep', 'hotfix-*'] + workflow_dispatch: + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..fe3cba9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,53 @@ +name: Lint + +on: + push: + branches: ['main', 'release-prep', 'hotfix-*'] + pull_request: + branches: ['main', 'release-prep', 'hotfix-*'] + +permissions: + contents: read + +jobs: + lint: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + steps: + - name: Checkout git repo + uses: actions/checkout@v4 + with: + path: extension-repo + + - name: Checkout paranext-core repo to use its sub-packages + uses: actions/checkout@v4 + with: + path: paranext-core + repository: paranext/paranext-core + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + cache: 'npm' + cache-dependency-path: | + extension-repo/package-lock.json + paranext-core/package-lock.json + node-version-file: extension-repo/package.json + + - name: Install extension dependencies + working-directory: extension-repo + run: npm ci + + - name: Install core dependencies + working-directory: paranext-core + run: npm ci --ignore-scripts + + - name: Run format checking + working-directory: extension-repo + run: npm run format:check + + - name: Run all linters + working-directory: extension-repo + run: npm run lint diff --git a/.vscode/launch.json b/.vscode/launch.json index d40ac68..7f01a07 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -44,7 +44,7 @@ "runtimeArgs": ["run", "start"], "skipFiles": ["/**"], "env": { - "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223", + "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223 --remote-allow-origins=http://localhost:9223", "IN_VSCODE": "true" }, "presentation": { diff --git a/README.md b/README.md index aa56a7e..213dea3 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ This is a Webpack project configured to build Platform.Bible extensions. The gen ## To install -### Install dependencies: +### Install dependencies 1. Follow the instructions to install [`paranext-core`](https://github.com/paranext/paranext-core#developer-install). We recommend you clone `paranext-core` in the same parent directory in which you cloned this repository so you do not have to [reconfigure paths](#configure-paths-to-paranext-core-repo) to `paranext-core`. 2. In this repo, run `npm install` to install local and published dependencies @@ -155,16 +155,16 @@ These steps will walk you through releasing a version on GitHub and bumping the
[Optional] Create a new pre-release and bump versions branch manually - #### Manually create a new pre-release and bump versions branch + ### Manually create a new pre-release and bump versions branch Alternatively, you can create a new pre-release manually: ```bash npm run package # Create a new pre-release in GitHub on tag `v` - # Copy `.github/assets/release-body.md` into the GitHub branch - # Generate changelog - # Attach contents of `release` folder + # Copy `.github/assets/release-body.md` into the release body + # Press the "Generate release notes" button in the release creation page to generate a changelog + # Attach contents of `release` folder to the release ``` Then bump versions by running the following: @@ -197,12 +197,12 @@ Following are some problems you may encounter while publishing and steps to solv If you see the following error in the GitHub Actions workflow logs while packaging: -``` +```bash Module build failed (from ./node_modules/swc-loader/src/index.js): Error: Failed to load native binding ``` -You may have a different effective version of `@swc/core` than `paranext-core` does. Please make sure the version of `@swc/core` in your `package-lock.json` is the same as its version in [`paranext-core/package-lock.json`](https://github.com/paranext/paranext-core/blob/main/package-lock.json). If they are not the same, please fix them to be the same by running `npm i -D @swc/core ` where the version is the version of `@swc/core` installed in `paranext-core/package-lock.json` (if you would like to set the version of `@swc/core` back to what it was before in `package.json` to stay synced with the extension template, change it back manually in `package.json` and then run `npm i`). If they are already the same, you may need to try regenerating your `package-lock.json` file by deleting it and running `npm i`. +You may have a different effective version of `@swc/core` than `paranext-core` does. Please make sure the version of `@swc/core` in your `package-lock.json` is the same as its version in [`paranext-core/package-lock.json`](https://github.com/paranext/paranext-core/blob/main/package-lock.json). If they are not the same, please fix them to be the same by running `npm i -D @swc/core@` where the version is the version of `@swc/core` installed in `paranext-core/package-lock.json` (if you would like to set the version of `@swc/core` back to what it was before in `package.json` to stay synced with the extension template, change it back manually in `package.json` and then run `npm i`). If they are already the same, you may need to try regenerating your `package-lock.json` file by deleting it and running `npm i`. ## To create a new extension in this repo @@ -214,15 +214,16 @@ npm run create-extension -- Then follow [the instructions for customizing the new extension](https://github.com/paranext/paranext-extension-template#customize-extension-details) with a few modifications: -- Follow the instructions for replacing placeholders inside the `src/` folder, not at this repo root, except in specific situations: - - Instead of editing the `.github/assets/release-body.md` inside the extension, add information about the new extension in `.github/assets/release-body.md` at this repo root. +- All of the places where it says to replace the extension name in [Replace Placeholders](https://github.com/paranext/paranext-extension-template#replace-placeholders) have been automated, + the other instructions there should apply inside the `src/` folder, not at this repo root. +- Instead of editing the `.github/assets/release-body.md` inside the extension, add information about the new extension in `.github/assets/release-body.md` at this repo root. **Note:** The merge/squash commits created when creating a new extension are important; Git uses them to compare the files for future updates. If you edit this repo's Git history, please preserve these commits (do not squash them, for example) to avoid duplicated merge conflicts in the future.
[Optional] Creating a new extension manually -#### Manually create a new extension +### Manually create a new extension Alternatively, you can create a new extension manually: @@ -238,11 +239,36 @@ the file paths pointing to `paranext-core`: - Find: `([^/])\.\.\/paranext-core` - Replace with: `$1../../../paranext-core` -You can ignore occurrences from many files. Please see [`./lib/git.util.ts`](./lib/git.util.ts) -> `formatExtensionFolder` for more -information. +You can ignore occurrences from many files. Please see [`./lib/git.util.ts`](./lib/git.util.ts) -> `formatExtensionFolder` for more information. + +Because these steps are not automated you need to follow all the instructions in [Replace Placeholders](https://github.com/paranext/paranext-extension-template#replace-placeholders)
+### Renaming an extension + +Renaming an extension involves more than just changing its folder name. Tools that track extension updates rely on the folder name to detect changes, so renaming must be done carefully to avoid duplicated diffs or future merge conflicts. + +**Note:** Unfortunately, this process effectively erases the history on all the files in this extension since they are being deleted and created anew from the perspective of the Git history. + +To safely rename an extension: + +1. [Update from the template](#to-update-this-repo-and-extensions-from-the-templates) to ensure the extension to be renamed has all latest changes (makes sure you don't revert any updates the template has received since you last updated the extension when you are copying the contents of the extension) + +2. Run the [`create-extension` script](#to-create-a-new-extension-in-this-repo) with the new name to create a new folder: + ```bash + npm run create-extension -- + ``` +3. Move the contents of the old extension into the new folder and delete the old folder. (If it's not already under source control, it would probably be wise to make a backup until you have confirmed that the rename was successful.) + +4. Update internal identifiers and references to match the new name (e.g., folder names, class names, package names, strings inside files). + +5. Test and commit the changes. + +This process ensures that template update comparisons continue to work correctly. + +**Note:** The merge/squash commits created when renaming an extension are important; Git uses them to compare the files for future updates. If you edit this repo's Git history, please preserve these commits (do not squash them, for example) to avoid duplicated merge conflicts in the future. + ## To update this repo and extensions from the templates This project is forked from [`paranext-multi-extension-template`](https://github.com/paranext/paranext-multi-extension-template), and its extensions are derived from [`paranext-extension-template`](https://github.com/paranext/paranext-extension-template). Both are updated periodically and will sometimes receive updates that help with breaking changes on [`paranext-core`](https://github.com/paranext/paranext-core). We recommend you periodically update your repo and extensions by merging the latest template updates into them. diff --git a/lib/create-extension.ts b/lib/create-extension.ts index 4d535f2..1cd3b6b 100644 --- a/lib/create-extension.ts +++ b/lib/create-extension.ts @@ -11,6 +11,12 @@ import { getExtensionPath } from '../webpack/webpack.util'; const newExtensionName = process.argv[2]; (async () => { + // verify that an extension name was provided + if (!newExtensionName) { + console.error(`No extension name provided.`); + return 1; + } + // Make sure there are not working changes as this will not work with working changes if (await checkForWorkingChanges()) return 1; @@ -43,7 +49,7 @@ const newExtensionName = process.argv[2]; // Don't commit for them so they know what is going on if (await checkForWorkingChanges(true)) console.log( - `After creating the extension at ${extensionPathOSIndependent} from ${SINGLE_TEMPLATE_NAME} and formatting it, there are working changes.\nThis is likely expected. Please commit the result.`, + `After creating the extension at ${extensionPathOSIndependent} from ${SINGLE_TEMPLATE_NAME} and formatting it, there are working changes.\nThis is expected. Please commit the result.`, ); return 0; diff --git a/lib/git.util.ts b/lib/git.util.ts index fb917fb..4f600c2 100644 --- a/lib/git.util.ts +++ b/lib/git.util.ts @@ -1,6 +1,7 @@ import { exec, ExecException, ExecOptions } from 'child_process'; import { promisify } from 'util'; import path from 'path'; +import fs from 'fs/promises'; import replaceInFile from 'replace-in-file'; const execAsync = promisify(exec); @@ -153,6 +154,37 @@ export async function fetchFromSingleTemplate() { return true; } +/** + * Converts kebab-case into camelCase. Assumes that the input is a valid kebab-case string + * + * Current implementation supports only UTF-16. + */ +function toCamelCaseFromKebab(input: string): string { + if (!input) return ''; + + // Split on common delimiters: hyphens, underscores, spaces, and dots + const parts = input.split('-'); + + // If there's only one part, return it as-is (already camelCase or single word) + if (parts.length <= 1) { + return input.charAt(0).toLocaleLowerCase() + input.slice(1); + } + + // Convert first part to lowercase, then capitalize first letter of subsequent parts + const camelCased = parts + .map((part, index) => { + if (!part) return ''; + + if (index === 0) { + return part.charAt(0).toLocaleLowerCase() + part.slice(1); + } + return part.charAt(0).toLocaleUpperCase() + part.slice(1); + }) + .join(''); + + return camelCased; +} + /** * Format an extension folder to make the extension template folder work as a subfolder of this repo * @@ -162,6 +194,10 @@ export async function fetchFromSingleTemplate() { * @param extensionFolderPath Path to the extension to format relative to root */ export async function formatExtensionFolder(extensionFolderPath: string) { + // Get the basename of the extension folder for use in replacements + const extensionName = path.basename(extensionFolderPath); + const extensionNameCamelCase = toCamelCaseFromKebab(extensionName); + // Replace ../paranext-core with ../../../paranext-core to fix ts-config and package.json and such const results = await replaceInFile({ files: `${extensionFolderPath}/**/*`, @@ -182,6 +218,7 @@ export async function formatExtensionFolder(extensionFolderPath: string) { countMatches: true, allowEmptyPaths: true, }); + const replaceStats = results.reduce( (replacements, replaceResult) => ({ totalReplacements: replacements.totalReplacements + (replaceResult.numReplacements ?? 0), @@ -194,6 +231,7 @@ export async function formatExtensionFolder(extensionFolderPath: string) { // eslint-disable-next-line no-type-assertion/no-type-assertion { totalReplacements: 0, filesChanged: [] as string[] }, ); + if (replaceStats.totalReplacements > 0) console.log( `Formatting ${extensionFolderPath}: Successfully updated relative path to paranext-core ${ @@ -202,4 +240,133 @@ export async function formatExtensionFolder(extensionFolderPath: string) { '\n\t', )}\n`, ); + + // Rename types file + const oldTypesFilePath = path.join( + extensionFolderPath, + 'src', + 'types', + 'paranext-extension-template.d.ts', + ); + const newTypesFilePath = path.join(extensionFolderPath, 'src', 'types', `${extensionName}.d.ts`); + + try { + // Check if the old file exists before attempting to rename it + await fs.access(oldTypesFilePath); + + // Check if the new file already exists to avoid errors + try { + await fs.access(newTypesFilePath); + console.log(`Types file already renamed to ${extensionName}.d.ts, skipping rename operation`); + } catch { + // New file doesn't exist, proceed with rename + await fs.rename(oldTypesFilePath, newTypesFilePath); + console.log(`Renamed types file to ${extensionName}.d.ts`); + } + } catch (error) { + // Old file doesn't exist, so no need to rename + console.log(`Types file paranext-extension-template.d.ts not found, skipping rename operation`); + } + + // Replace occurrences of 'paranext-extension-template' in the renamed types file + try { + await fs.access(newTypesFilePath); + + // Read the types file content + let typesFileContent = await fs.readFile(newTypesFilePath, 'utf8'); + + // Replace all occurrences of the template name + typesFileContent = typesFileContent.replace(/paranext-extension-template/g, extensionName); + + // Write the updated content back to the file + await fs.writeFile(newTypesFilePath, typesFileContent, 'utf8'); + + console.log(`Updated module declaration and references in types file`); + } catch (error) { + console.error(`Could not update types file: ${error.message}`); + } + + // Update README.md + const readmePath = path.join(extensionFolderPath, 'README.md'); + try { + // Check if README.md exists + await fs.access(readmePath); + + const readmeContent = await fs.readFile(readmePath, 'utf8'); + const lines = readmeContent.split('\n'); + + // Identify section boundaries + const endOfTitle = lines.findIndex((line) => line.indexOf('## Template Info') >= 0); + const summary = lines.findIndex((line, n) => n > endOfTitle && line.indexOf('# Summary') >= 0); + const endOfSummary = lines.findIndex((line, n) => n > summary && line.startsWith('##')); + + if (endOfTitle < 0 || summary < 0 || endOfSummary < 0 || endOfTitle > summary) { + console.error( + `Error identifying Template Info in README.md, formatExtensionFolder outdated?`, + ); + return; + } + + // Split the README into sections to change, and sections to leave alone + const titleSection = lines.slice(0, endOfTitle); + const betweenTitleAndSummary = lines.slice(endOfTitle, summary); + const summarySection = lines.slice(summary, endOfSummary); + const after = lines.slice(endOfSummary); + + // Modify only the `titleSection` and `summarySection` + const modifiedTitle = titleSection.map((line) => + line.replace(/paranext-extension-template/g, extensionName), + ); + const modifiedSummary = summarySection.map((line) => + line.replace(/paranext-extension-template/g, extensionName), + ); + + // Reconstruct the README + const finalLines = [...modifiedTitle, ...betweenTitleAndSummary, ...modifiedSummary, ...after]; + + await fs.writeFile(readmePath, finalLines.join('\n'), 'utf8'); + console.log(`Updated README.md: modified title and summary sections only`); + } catch (error) { + console.error(`Could not update README.md: ${error.message}`); + } + + // Update manifest.json + const manifestPath = path.join(extensionFolderPath, 'manifest.json'); + try { + // Check if manifest.json exists + await fs.access(manifestPath); + + let manifestContent = await fs.readFile(manifestPath, 'utf8'); + + // Replace "paranextExtensionTemplate" with lowerCamelCase version of extension name + manifestContent = manifestContent.replace(/paranextExtensionTemplate/g, extensionNameCamelCase); + + // Replace the type reference + manifestContent = manifestContent.replace( + /src\/types\/paranext-extension-template\.d\.ts/g, + `src/types/${extensionName}.d.ts`, + ); + + await fs.writeFile(manifestPath, manifestContent, 'utf8'); + console.log(`Updated manifest.json with ${extensionName} information`); + } catch (error) { + console.error(`Could not update manifest.json: ${error.message}`); + } + + // Update package.json + const packagePath = path.join(extensionFolderPath, 'package.json'); + try { + // Check if package.json exists + await fs.access(packagePath); + + let packageContent = await fs.readFile(packagePath, 'utf8'); + + // Replace all occurrences of "paranext-extension-template" with extensionName + packageContent = packageContent.replace(/paranext-extension-template/g, extensionName); + + await fs.writeFile(packagePath, packageContent, 'utf8'); + console.log(`Updated package.json with ${extensionName} information`); + } catch (error) { + console.error(`Could not update package.json: ${error.message}`); + } } diff --git a/package-lock.json b/package-lock.json index 65fcafe..40c4752 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "platform-bible-utils": "file:../paranext-core/lib/platform-bible-utils" }, "devDependencies": { - "@swc/core": "^1.10.18", + "@swc/core": "1.10.18", "@tailwindcss/typography": "^0.5.16", "@types/node": "^20.16.11", "@types/react": "^18.3.18", @@ -85,8 +85,9 @@ "platform-bible-utils": "file:../platform-bible-utils" }, "devDependencies": { + "electron-log": "^5.0.3", "escape-string-regexp": "^5.0.0", - "typescript": "^5.4.5" + "typescript": "5.4.5" } }, "../paranext-core/lib/platform-bible-react": { @@ -94,25 +95,26 @@ "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", - "@tanstack/react-table": "^8.20.5", + "@radix-ui/react-avatar": "^1.1.9", + "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-dropdown-menu": "^2.1.14", + "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-menubar": "^1.1.14", + "@radix-ui/react-popover": "^1.1.13", + "@radix-ui/react-radio-group": "^1.3.6", + "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-separator": "^1.1.6", + "@radix-ui/react-slider": "^1.3.4", + "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-toast": "^1.2.13", + "@radix-ui/react-toggle": "^1.1.8", + "@radix-ui/react-toggle-group": "^1.1.9", + "@radix-ui/react-tooltip": "^1.2.6", + "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/react-table": "^8.21.3", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -130,7 +132,11 @@ "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", + "@chromatic-com/storybook": "^4.0.0", "@senojs/rollup-plugin-style-inject": "^0.2.3", + "@storybook/addon-a11y": "^9.0.6", + "@storybook/addon-links": "^9.0.6", + "@storybook/react-vite": "^9.0.6", "@tailwindcss/typography": "^0.5.16", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -142,14 +148,17 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitejs/plugin-react-swc": "^3.8.0", + "axe-playwright": "^2.1.0", "dts-bundle-generator": "^9.5.1", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-storybook": "^9.0.6", "jsonpath-plus": "^10.3.0", "prettier": "^3.5.2", "prettier-plugin-jsdoc": "^1.3.2", "prettier-plugin-tailwindcss": "^0.6.11", + "storybook": "^9.0.5", "stylelint": "^16.11.0", "stylelint-config-recommended": "^14.0.1", "stylelint-config-sass-guidelines": "^12.1.0", @@ -157,9 +166,9 @@ "tailwindcss": "^3.4.17", "tailwindcss-scoped-preflight": "^2.2.4", "tslib": "^2.8.1", - "typedoc": "^0.27.9", - "typescript": "^5.4.5", - "vite": "^6.2.5", + "typedoc": "^0.28.2", + "typescript": "^5.8.3", + "vite": "^6.3.4", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.7" }, @@ -176,7 +185,7 @@ "jsonpath-plus": "^10.3.0" }, "devDependencies": { - "@biblionexus-foundation/scripture-utilities": "^0.0.8", + "@biblionexus-foundation/scripture-utilities": "~0.1.0", "@types/jest": "^29.5.14", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -189,9 +198,9 @@ "prettier-plugin-jsdoc": "^1.3.2", "stringz": "^2.1.0", "tslib": "^2.8.1", - "typedoc": "^0.27.9", - "typescript": "^5.4.5", - "vite": "^6.2.5", + "typedoc": "^0.28.3", + "typescript": "^5.8.3", + "vite": "^6.3.4", "vitest": "^3.0.7" } }, @@ -21163,9 +21172,10 @@ "papi-dts": { "version": "file:../paranext-core/lib/papi-dts", "requires": { + "electron-log": "^5.0.3", "escape-string-regexp": "^5.0.0", "platform-bible-utils": "file:../platform-bible-utils", - "typescript": "^5.4.5" + "typescript": "5.4.5" } }, "parent-module": { @@ -21315,27 +21325,32 @@ "@babel/preset-env": "^7.26.9", "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slider": "^1.2.3", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.8", + "@chromatic-com/storybook": "^4.0.0", + "@radix-ui/react-avatar": "^1.1.9", + "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-dropdown-menu": "^2.1.14", + "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-menubar": "^1.1.14", + "@radix-ui/react-popover": "^1.1.13", + "@radix-ui/react-radio-group": "^1.3.6", + "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-separator": "^1.1.6", + "@radix-ui/react-slider": "^1.3.4", + "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-toast": "^1.2.13", + "@radix-ui/react-toggle": "^1.1.8", + "@radix-ui/react-toggle-group": "^1.1.9", + "@radix-ui/react-tooltip": "^1.2.6", "@senojs/rollup-plugin-style-inject": "^0.2.3", + "@storybook/addon-a11y": "^9.0.6", + "@storybook/addon-links": "^9.0.6", + "@storybook/react-vite": "^9.0.6", + "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.16", - "@tanstack/react-table": "^8.20.5", + "@tanstack/react-table": "^8.21.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", @@ -21347,6 +21362,7 @@ "@typescript-eslint/parser": "^6.21.0", "@vitejs/plugin-react-swc": "^3.8.0", "autoprefixer": "^10.4.20", + "axe-playwright": "^2.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -21354,6 +21370,7 @@ "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-storybook": "^9.0.6", "jsonpath-plus": "^10.3.0", "lucide-react": "^0.475.0", "markdown-to-jsx": "^7.7.4", @@ -21364,6 +21381,7 @@ "prettier-plugin-tailwindcss": "^0.6.11", "react-hotkeys-hook": "^4.6.1", "sonner": "^1.7.4", + "storybook": "^9.0.5", "stylelint": "^16.11.0", "stylelint-config-recommended": "^14.0.1", "stylelint-config-sass-guidelines": "^12.1.0", @@ -21373,9 +21391,9 @@ "tailwindcss-animate": "^1.0.7", "tailwindcss-scoped-preflight": "^2.2.4", "tslib": "^2.8.1", - "typedoc": "^0.27.9", - "typescript": "^5.4.5", - "vite": "^6.2.5", + "typedoc": "^0.28.2", + "typescript": "^5.8.3", + "vite": "^6.3.4", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.7" } @@ -21383,7 +21401,7 @@ "platform-bible-utils": { "version": "file:../paranext-core/lib/platform-bible-utils", "requires": { - "@biblionexus-foundation/scripture-utilities": "^0.0.8", + "@biblionexus-foundation/scripture-utilities": "~0.1.0", "@types/jest": "^29.5.14", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -21398,9 +21416,9 @@ "prettier-plugin-jsdoc": "^1.3.2", "stringz": "^2.1.0", "tslib": "^2.8.1", - "typedoc": "^0.27.9", - "typescript": "^5.4.5", - "vite": "^6.2.5", + "typedoc": "^0.28.3", + "typescript": "^5.8.3", + "vite": "^6.3.4", "vitest": "^3.0.7" } }, diff --git a/package.json b/package.json index 8d36454..4f17aa0 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "start:production": "cross-env MAIN_ARGS=\"--extensionDirs $INIT_CWD/dist\" concurrently \"npm:watch:production\" \"npm:start:core\"", "lint": "npm run lint:scripts && npm run lint:styles", "lint:scripts": "cross-env NODE_ENV=development eslint --ext .cjs,.js,.jsx,.ts,.tsx --cache .", - "lint:styles": "stylelint **/*.{css,scss}", + "lint:styles": "stylelint **/*.{css,scss} --allow-empty-input", "lint-fix": "npm run lint-fix:scripts && npm run lint:styles -- --fix", "lint-fix:scripts": "npm run format && npm run lint:scripts", "postinstall": "tsx ./lib/add-remotes.ts", @@ -39,7 +39,7 @@ "platform-bible-utils": "file:../paranext-core/lib/platform-bible-utils" }, "devDependencies": { - "@swc/core": "^1.10.18", + "@swc/core": "1.10.18", "@tailwindcss/typography": "^0.5.16", "@types/node": "^20.16.11", "@types/react": "^18.3.18", @@ -94,9 +94,7 @@ "webpack-merge": "^6.0.1", "zip-folder-promise": "^1.2.0" }, - "workspaces": [ - "src/*" - ], + "workspaces": ["src/*"], "volta": { "node": "20.18.0" } diff --git a/src/scripture-forge/manifest.json b/src/scripture-forge/manifest.json index 3711895..e25652a 100644 --- a/src/scripture-forge/manifest.json +++ b/src/scripture-forge/manifest.json @@ -7,9 +7,7 @@ "license": "MIT", "main": "src/main.ts", "extensionDependencies": {}, - "elevatedPrivileges": [ - "handleUri" - ], + "elevatedPrivileges": ["handleUri"], "types": "src/types/scripture-forge.d.ts", "menus": "contributions/menus.json", "settings": "contributions/settings.json", diff --git a/webpack/webpack.config.base.ts b/webpack/webpack.config.base.ts index 147da40..2c1e0cc 100644 --- a/webpack/webpack.config.base.ts +++ b/webpack/webpack.config.base.ts @@ -69,7 +69,9 @@ const configBase: webpack.Configuration = { // This must be the first rule in order to be applied after all other transformations // https://webpack.js.org/guides/asset-modules/#replacing-inline-loader-syntax { - resourceQuery: /inline/, + resourceQuery: { + and: [/inline/, { not: [/raw/] }], + }, type: 'asset/source', }, // Load TypeScript with SWC https://swc.rs/docs/usage/swc-loader @@ -77,6 +79,7 @@ const configBase: webpack.Configuration = { // https://github.com/TypeStrong/ts-loader#options { test: /\.tsx?$/, + resourceQuery: { not: [/raw/] }, use: { loader: 'swc-loader', options: { @@ -99,6 +102,7 @@ const configBase: webpack.Configuration = { // https://webpack.js.org/loaders/sass-loader/#getting-started { test: /\.(sa|sc|c)ss$/, + resourceQuery: { not: [/raw/] }, use: [ // We are not using style-loader since we are passing styles to papi, not inserting them // into dom. style-loader would add html style elements for our styles if we used it @@ -118,6 +122,7 @@ const configBase: webpack.Configuration = { // https://webpack.js.org/guides/asset-management/#loading-images { test: /\.(png|svg|jpg|jpeg|gif)$/i, + resourceQuery: { not: [/raw/] }, type: 'asset/inline', }, /** @@ -128,6 +133,7 @@ const configBase: webpack.Configuration = { // https://webpack.js.org/guides/asset-management/#loading-fonts { test: /\.(woff|woff2|eot|ttf|otf)$/i, + resourceQuery: { not: [/raw/] }, type: 'asset/inline', }, /** Import files with no transformation as strings with "./file?raw" */ diff --git a/webpack/webpack.util.ts b/webpack/webpack.util.ts index 15e3029..6564cd5 100644 --- a/webpack/webpack.util.ts +++ b/webpack/webpack.util.ts @@ -142,7 +142,9 @@ const staticFiles: { { from: '', noErrorOnMissing: true }, // Copy the localized strings JSON file into the output folder based on its listing in `manifest.localizedStrings` { from: '', noErrorOnMissing: true }, - // Copy the display data JSON file into the output folder based on its listing in `manifest.localizedStrings` + // Copy the themes JSON file into the output folder based on its listing in `manifest.themes` + { from: '', noErrorOnMissing: true }, + // Copy the display data JSON file into the output folder based on its listing in `manifest.displayData` { from: '', noErrorOnMissing: true }, ]; @@ -155,6 +157,7 @@ function getStaticFileName(staticFile: string, extensionInfo: ExtensionInfo) { .replace(//g, extensionInfo.settings ?? '') .replace(//g, extensionInfo.projectSettings ?? '') .replace(//g, extensionInfo.localizedStrings ?? '') + .replace(//g, extensionInfo.themes ?? '') .replace(//g, extensionInfo.displayData ?? ''); } @@ -301,6 +304,8 @@ type ExtensionManifest = { projectSettings?: string; /** Path to the JSON file that defines the localized strings this extension is adding. */ localizedStrings?: string; + /** Path to the JSON file that defines the themes this extension is adding. */ + themes?: string; /** Path to the JSON file that defines the localized display data this extension is adding. */ displayData?: string; activationEvents: string[];