Unified · Split
| @@ -30,6 +30,7 @@ dist-electron/ · 29 unmodified lines | ||
| 30 | 30 | .electron-runtime/ |
| 31 | 31 | .showcase/ |
| 32 | 32 | apps/mobile/.showcase/ |
| 33 | +apps/mobile/.generated/ | |
| 33 | 34 | artifacts/app-store/screenshots/ |
| 34 | 35 | .github/pr-assets/ |
| 35 | 36 | native/**/target/ |
| @@ -0,0 +1,23 @@ | ||
| 1 | +badgin is a refactored fork of Tinycon. | |
| 2 | + | |
| 3 | +MIT License | |
| 4 | + | |
| 5 | +Copyright (c) Julian Hundeloh and Tinycon contributors | |
| 6 | + | |
| 7 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 8 | +of this software and associated documentation files (the "Software"), to deal | |
| 9 | +in the Software without restriction, including without limitation the rights | |
| 10 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 11 | +copies of the Software, and to permit persons to whom the Software is | |
| 12 | +furnished to do so, subject to the following conditions: | |
| 13 | + | |
| 14 | +The above copyright notice and this permission notice shall be included in all | |
| 15 | +copies or substantial portions of the Software. | |
| 16 | + | |
| 17 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 18 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 19 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 20 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 21 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 22 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 23 | +SOFTWARE. | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +Copyright (c) 2014-2015, Felix Boehm <me@feedic.com> | |
| 2 | + | |
| 3 | +Permission to use, copy, modify, and/or distribute this software for any | |
| 4 | +purpose with or without fee is hereby granted, provided that the above | |
| 5 | +copyright notice and this permission notice appear in all copies. | |
| 6 | + | |
| 7 | +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
| 8 | +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
| 9 | +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |
| 10 | +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
| 11 | +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
| 12 | +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |
| 13 | +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +The MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2012 Near Infinity Corporation | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining | |
| 6 | +a copy of this software and associated documentation files (the | |
| 7 | +"Software"), to deal in the Software without restriction, including | |
| 8 | +without limitation the rights to use, copy, modify, merge, publish, | |
| 9 | +distribute, sublicense, and/or sell copies of the Software, and to | |
| 10 | +permit persons to whom the Software is furnished to do so, subject to | |
| 11 | +the following conditions: | |
| 12 | + | |
| 13 | +The above copyright notice and this permission notice shall be | |
| 14 | +included in all copies or substantial portions of the Software. | |
| 15 | + | |
| 16 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 17 | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 18 | +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| 19 | +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
| 20 | +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
| 21 | +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
| 22 | +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) Dave Wasmer | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,29 @@ | ||
| 1 | +BSD 3-Clause License | |
| 2 | + | |
| 3 | +Copyright (c) 2016-present, 650 Industries, Inc. (Expo) | |
| 4 | +All rights reserved. | |
| 5 | + | |
| 6 | +Redistribution and use in source and binary forms, with or without | |
| 7 | +modification, are permitted provided that the following conditions are met: | |
| 8 | + | |
| 9 | +1. Redistributions of source code must retain the above copyright notice, this | |
| 10 | + list of conditions and the following disclaimer. | |
| 11 | + | |
| 12 | +2. Redistributions in binary form must reproduce the above copyright notice, | |
| 13 | + this list of conditions and the following disclaimer in the documentation | |
| 14 | + and/or other materials provided with the distribution. | |
| 15 | + | |
| 16 | +3. Neither the name of the copyright holder nor the names of its | |
| 17 | + contributors may be used to endorse or promote products derived from | |
| 18 | + this software without specific prior written permission. | |
| 19 | + | |
| 20 | +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| 21 | +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 22 | +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| 23 | +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| 24 | +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| 25 | +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| 26 | +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| 27 | +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| 28 | +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 29 | +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) Meta Platforms, Inc. and affiliates. | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,32 @@ | ||
| 1 | +GhosttyKit | |
| 2 | + | |
| 3 | +The iOS terminal renderer vendors GhosttyKit.xcframework, a libghostty build produced from T3's | |
| 4 | +iOS 16 support fork. That fork was created from VVTerm's custom-I/O Ghostty fork. | |
| 5 | + | |
| 6 | +Upstream project: https://github.com/ghostty-org/ghostty | |
| 7 | +Custom-I/O base fork: https://github.com/wiedymi/ghostty/tree/custom-io | |
| 8 | +Vendored source fork: https://github.com/Yash-Singh1/ghostty/tree/custom-io | |
| 9 | +Vendored revision: d36c3b8dffd0d756dd5e5f4933962f774a0e6753 | |
| 10 | +Reference integration: https://github.com/vivy-company/vvterm | |
| 11 | + | |
| 12 | +MIT License | |
| 13 | + | |
| 14 | +Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors | |
| 15 | + | |
| 16 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 17 | +of this software and associated documentation files (the "Software"), to deal | |
| 18 | +in the Software without restriction, including without limitation the rights | |
| 19 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 20 | +copies of the Software, and to permit persons to whom the Software is | |
| 21 | +furnished to do so, subject to the following conditions: | |
| 22 | + | |
| 23 | +The above copyright notice and this permission notice shall be included in all | |
| 24 | +copies or substantial portions of the Software. | |
| 25 | + | |
| 26 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 27 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 28 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 29 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 30 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 31 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 32 | +SOFTWARE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2018 Oliver Moran | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +Copyright 2009, 2010, 2013 André Berg | |
| 2 | + | |
| 3 | +Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +you may not use this file except in compliance with the License. | |
| 5 | +You may obtain a copy of the License at | |
| 6 | + | |
| 7 | +http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | + | |
| 9 | +Unless required by applicable law or agreed to in writing, software | |
| 10 | +distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +See the License for the specific language governing permissions and | |
| 13 | +limitations under the License. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) Meta Platforms, Inc. and affiliates. | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2024-2025 Szymon Rybczak | |
| 4 | +Copyright (c) 2025-present Callstack | |
| 5 | + | |
| 6 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 7 | +of this software and associated documentation files (the "Software"), to deal | |
| 8 | +in the Software without restriction, including without limitation the rights | |
| 9 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 10 | +copies of the Software, and to permit persons to whom the Software is | |
| 11 | +furnished to do so, subject to the following conditions: | |
| 12 | + | |
| 13 | +The above copyright notice and this permission notice shall be included in all | |
| 14 | +copies or substantial portions of the Software. | |
| 15 | + | |
| 16 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 17 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 18 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 19 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 20 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 21 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 22 | +SOFTWARE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2024 Marc Rousavy | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) Anton Korzunov <thekashey@gmail.com> | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2026 React Navigation Contributors | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,24 @@ | ||
| 1 | +This is free and unencumbered software released into the public domain. | |
| 2 | + | |
| 3 | +Anyone is free to copy, modify, publish, use, compile, sell, or | |
| 4 | +distribute this software, either in source code form or as a compiled | |
| 5 | +binary, for any purpose, commercial or non-commercial, and by any | |
| 6 | +means. | |
| 7 | + | |
| 8 | +In jurisdictions that recognize copyright laws, the author or authors | |
| 9 | +of this software dedicate any and all copyright interest in the | |
| 10 | +software to the public domain. We make this dedication for the benefit | |
| 11 | +of the public at large and to the detriment of our heirs and | |
| 12 | +successors. We intend this dedication to be an overt act of | |
| 13 | +relinquishment in perpetuity of all present and future rights to this | |
| 14 | +software under copyright law. | |
| 15 | + | |
| 16 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 17 | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 18 | +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
| 19 | +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
| 20 | +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
| 21 | +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
| 22 | +OTHER DEALINGS IN THE SOFTWARE. | |
| 23 | + | |
| 24 | +For more information, please refer to <http://unlicense.org/> | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2018 Evert Pot | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | const fs = require("node:fs"); |
| 2 | 2 | const path = require("node:path"); |
| 3 | +const { pathToFileURL } = require("node:url"); | |
| 4 | +const TypeScript = require("typescript"); | |
| 3 | 5 | const { getDefaultConfig } = require("expo/metro-config"); |
| 4 | 6 | const { withUniwindConfig } = require("uniwind/metro"); |
| 5 | 7 | const extraThemes = require("./generated-uniwind-theme-names.json"); |
| @@ -7,6 +9,13 @@ const extraThemes = require("./generated-uniwind-theme-names.json"); · 1 unmodified line | ||
| 7 | 9 | /** @type {import("expo/metro-config").MetroConfig} */ |
| 8 | 10 | const config = getDefaultConfig(__dirname); |
| 9 | 11 | const workspaceRoot = path.resolve(__dirname, "../.."); |
| 12 | +const generatedLicenseModuleRoot = path.join(__dirname, ".generated", "third-party-licenses"); | |
| 13 | +const licenseGeneratorSource = path.join( | |
| 14 | + workspaceRoot, | |
| 15 | + "scripts", | |
| 16 | + "lib", | |
| 17 | + "third-party-licenses.ts", | |
| 18 | +); | |
| 10 | 19 | const escapedWorkspaceRoot = workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| 11 | 20 | const mobileShikiRoot = path.dirname(require.resolve("shiki/package.json", { paths: [__dirname] })); |
| 12 | 21 | const resolveShikiDependencyRoot = (packageName) => { |
| @@ -38,6 +47,7 @@ config.resolver = { · 25 unmodified lines | ||
| 38 | 47 | extraNodeModules: { |
| 39 | 48 | // oxlint-disable-next-line unicorn/no-useless-fallback-in-spread |
| 40 | 49 | ...(config.resolver?.extraNodeModules ?? {}), |
| 50 | + "@t3tools/mobile-third-party-licenses": generatedLicenseModuleRoot, | |
| 41 | 51 | shiki: mobileShikiRoot, |
| 42 | 52 | "@shikijs/core": resolveShikiDependencyRoot("@shikijs/core"), |
| 43 | 53 | "@shikijs/engine-javascript": resolveShikiDependencyRoot("@shikijs/engine-javascript"), |
| @@ -49,8 +59,54 @@ config.resolver = { · 5 unmodified lines | ||
| 49 | 59 | }, |
| 50 | 60 | }; |
| 51 | 61 | |
| 52 | −module.exports = withUniwindConfig(config, { | |
| 53 | − cssEntryFile: "./global.css", | |
| 54 | − extraThemes, | |
| 55 | − polyfills: { rem: 14 }, | |
| 56 | −}); | |
| 62 | +async function writeFileIfChanged(filePath, contents) { | |
| 63 | + try { | |
| 64 | + if ((await fs.promises.readFile(filePath, "utf8")) === contents) return; | |
| 65 | + } catch (error) { | |
| 66 | + if (error?.code !== "ENOENT") throw error; | |
| 67 | + } | |
| 68 | + await fs.promises.writeFile(filePath, contents, "utf8"); | |
| 69 | +} | |
| 70 | + | |
| 71 | +async function generateMobileThirdPartyLicenses() { | |
| 72 | + await fs.promises.mkdir(generatedLicenseModuleRoot, { recursive: true }); | |
| 73 | + const generatorSource = await fs.promises.readFile(licenseGeneratorSource, "utf8"); | |
| 74 | + const compiledGenerator = TypeScript.transpileModule(generatorSource, { | |
| 75 | + compilerOptions: { | |
| 76 | + module: TypeScript.ModuleKind.ESNext, | |
| 77 | + target: TypeScript.ScriptTarget.ES2022, | |
| 78 | + verbatimModuleSyntax: true, | |
| 79 | + }, | |
| 80 | + fileName: licenseGeneratorSource, | |
| 81 | + }).outputText; | |
| 82 | + const compiledGeneratorPath = path.join(generatedLicenseModuleRoot, "generator.mjs"); | |
| 83 | + await writeFileIfChanged(compiledGeneratorPath, compiledGenerator); | |
| 84 | + | |
| 85 | + const generatorVersion = (await fs.promises.stat(compiledGeneratorPath)).mtimeMs; | |
| 86 | + const { generateThirdPartyLicenseManifest } = await import( | |
| 87 | + `${pathToFileURL(compiledGeneratorPath).href}?version=${String(generatorVersion)}` | |
| 88 | + ); | |
| 89 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 90 | + configFile: path.join(workspaceRoot, "third-party-licenses.config.json"), | |
| 91 | + packageManifests: [{ bundle: "mobile", path: path.join(__dirname, "package.json") }], | |
| 92 | + }); | |
| 93 | + | |
| 94 | + await Promise.all([ | |
| 95 | + writeFileIfChanged( | |
| 96 | + path.join(generatedLicenseModuleRoot, "index.js"), | |
| 97 | + `module.exports = ${JSON.stringify(manifest)};\n`, | |
| 98 | + ), | |
| 99 | + writeFileIfChanged( | |
| 100 | + path.join(generatedLicenseModuleRoot, "package.json"), | |
| 101 | + '{"main":"index.js"}\n', | |
| 102 | + ), | |
| 103 | + ]); | |
| 104 | +} | |
| 105 | + | |
| 106 | +module.exports = generateMobileThirdPartyLicenses().then(() => | |
| 107 | + withUniwindConfig(config, { | |
| 108 | + cssEntryFile: "./global.css", | |
| 109 | + extraThemes, | |
| 110 | + polyfills: { rem: 14 }, | |
| 111 | + }), | |
| 112 | +); | |
| @@ -55,6 +55,10 @@ import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsCl · 54 unmodified lines | ||
| 55 | 55 | import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; |
| 56 | 56 | import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; |
| 57 | 57 | import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen"; |
| 58 | +import { | |
| 59 | + SettingsOpenSourceLicenseRouteScreen, | |
| 60 | + SettingsOpenSourceLicensesRouteScreen, | |
| 61 | +} from "./features/settings/SettingsOpenSourceLicensesRouteScreen"; | |
| 58 | 62 | import { SettingsProjectGroupingRouteScreen } from "./features/settings/SettingsProjectGroupingRouteScreen"; |
| 59 | 63 | import { UsageRouteScreen } from "./features/usage/UsageRouteScreen"; |
| 60 | 64 | import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen"; |
| @@ -191,6 +195,20 @@ const SettingsContentStack = createNativeStackNavigator({ · 130 unmodified lines | ||
| 191 | 195 | title: "Client Storage", |
| 192 | 196 | }, |
| 193 | 197 | }), |
| 198 | + SettingsOpenSourceLicenses: createNativeStackScreen({ | |
| 199 | + screen: SettingsOpenSourceLicensesRouteScreen, | |
| 200 | + linking: "open-source-licenses", | |
| 201 | + options: { | |
| 202 | + title: "Open source licenses", | |
| 203 | + }, | |
| 204 | + }), | |
| 205 | + SettingsOpenSourceLicense: createNativeStackScreen({ | |
| 206 | + screen: SettingsOpenSourceLicenseRouteScreen, | |
| 207 | + linking: "open-source-licenses/:entryKey", | |
| 208 | + options: { | |
| 209 | + title: "License notice", | |
| 210 | + }, | |
| 211 | + }), | |
| 194 | 212 | SettingsUsage: createNativeStackScreen({ |
| 195 | 213 | screen: UsageRouteScreen, |
| 196 | 214 | linking: "usage", |
| @@ -0,0 +1,212 @@ | ||
| 1 | +import { LegendList } from "@legendapp/list/react-native"; | |
| 2 | +import { type StaticScreenProps, useNavigation } from "@react-navigation/native"; | |
| 3 | +import { | |
| 4 | + filterThirdPartyLicenseEntries, | |
| 5 | + findThirdPartyLicenseEntry, | |
| 6 | + formatLicenseBundles, | |
| 7 | + thirdPartyLicenseEntryKey, | |
| 8 | + type ThirdPartyLicenseEntry, | |
| 9 | +} from "@t3tools/shared/thirdPartyLicenses"; | |
| 10 | +import { useCallback, useMemo, useState } from "react"; | |
| 11 | +import { Linking, Platform, Pressable, ScrollView, View } from "react-native"; | |
| 12 | +import { useSafeAreaInsets } from "react-native-safe-area-context"; | |
| 13 | + | |
| 14 | +import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | |
| 15 | +import { SymbolView } from "../../components/AppSymbol"; | |
| 16 | +import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; | |
| 17 | +import { NativeStackScreenOptions } from "../../native/StackHeader"; | |
| 18 | +import { MOBILE_THIRD_PARTY_LICENSES } from "./mobileThirdPartyLicenses"; | |
| 19 | + | |
| 20 | +function LicenseRow(props: { | |
| 21 | + readonly entry: ThirdPartyLicenseEntry; | |
| 22 | + readonly onPress: () => void; | |
| 23 | +}) { | |
| 24 | + return ( | |
| 25 | + <Pressable | |
| 26 | + accessibilityHint="Opens the complete license notice" | |
| 27 | + accessibilityLabel={`${props.entry.name}, ${props.entry.license}`} | |
| 28 | + accessibilityRole="button" | |
| 29 | + onPress={props.onPress} | |
| 30 | + className="border-b border-border bg-card px-5 py-4 active:bg-card-alt" | |
| 31 | + > | |
| 32 | + <View className="flex-row items-start gap-3"> | |
| 33 | + <View className="min-w-0 flex-1 gap-1"> | |
| 34 | + <Text className="text-base font-t3-medium text-foreground" numberOfLines={2}> | |
| 35 | + {props.entry.name} | |
| 36 | + </Text> | |
| 37 | + <Text className="text-sm text-foreground-muted" numberOfLines={2}> | |
| 38 | + {props.entry.version ? `${props.entry.version} · ` : ""} | |
| 39 | + {props.entry.license} | |
| 40 | + </Text> | |
| 41 | + </View> | |
| 42 | + <SymbolView | |
| 43 | + name="chevron.right" | |
| 44 | + size={16} | |
| 45 | + tintColorClassName={"accent-chevron"} | |
| 46 | + type="monochrome" | |
| 47 | + weight="semibold" | |
| 48 | + /> | |
| 49 | + </View> | |
| 50 | + </Pressable> | |
| 51 | + ); | |
| 52 | +} | |
| 53 | + | |
| 54 | +export function SettingsOpenSourceLicensesRouteScreen() { | |
| 55 | + const navigation = useNavigation(); | |
| 56 | + const insets = useSafeAreaInsets(); | |
| 57 | + const [query, setQuery] = useState(""); | |
| 58 | + const entries = MOBILE_THIRD_PARTY_LICENSES.entries; | |
| 59 | + const filteredEntries = useMemo( | |
| 60 | + () => filterThirdPartyLicenseEntries(entries, query), | |
| 61 | + [entries, query], | |
| 62 | + ); | |
| 63 | + const renderItem = useCallback( | |
| 64 | + ({ item }: { readonly item: ThirdPartyLicenseEntry }) => ( | |
| 65 | + <LicenseRow | |
| 66 | + entry={item} | |
| 67 | + onPress={() => | |
| 68 | + navigation.navigate("SettingsSheet", { | |
| 69 | + screen: "SettingsContent", | |
| 70 | + params: { | |
| 71 | + screen: "SettingsOpenSourceLicense", | |
| 72 | + params: { entryKey: thirdPartyLicenseEntryKey(item) }, | |
| 73 | + }, | |
| 74 | + }) | |
| 75 | + } | |
| 76 | + /> | |
| 77 | + ), | |
| 78 | + [navigation], | |
| 79 | + ); | |
| 80 | + | |
| 81 | + return ( | |
| 82 | + <View collapsable={false} className="flex-1 bg-sheet"> | |
| 83 | + {Platform.OS === "android" ? ( | |
| 84 | + <> | |
| 85 | + <NativeStackScreenOptions options={{ headerShown: false }} /> | |
| 86 | + <AndroidScreenHeader title="Open source licenses" onBack={() => navigation.goBack()} /> | |
| 87 | + </> | |
| 88 | + ) : null} | |
| 89 | + <LegendList | |
| 90 | + className="flex-1" | |
| 91 | + contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }} | |
| 92 | + contentInsetAdjustmentBehavior="automatic" | |
| 93 | + data={filteredEntries} | |
| 94 | + estimatedItemSize={78} | |
| 95 | + keyboardDismissMode="on-drag" | |
| 96 | + keyboardShouldPersistTaps="handled" | |
| 97 | + keyExtractor={thirdPartyLicenseEntryKey} | |
| 98 | + ListEmptyComponent={ | |
| 99 | + <View className="items-center px-6 py-12"> | |
| 100 | + <Text className="text-center text-base text-foreground-muted"> | |
| 101 | + No licenses match that search. | |
| 102 | + </Text> | |
| 103 | + </View> | |
| 104 | + } | |
| 105 | + ListHeaderComponent={ | |
| 106 | + <View className="gap-4 px-5 pt-4 pb-5"> | |
| 107 | + <Text className="text-base leading-normal text-foreground-muted"> | |
| 108 | + Third-party notices for dependencies and assets included in T3 Code Mobile. | |
| 109 | + </Text> | |
| 110 | + <TextInput | |
| 111 | + accessibilityLabel="Search open-source licenses" | |
| 112 | + autoCapitalize="none" | |
| 113 | + autoCorrect={false} | |
| 114 | + clearButtonMode="while-editing" | |
| 115 | + onChangeText={setQuery} | |
| 116 | + placeholder="Search packages" | |
| 117 | + returnKeyType="search" | |
| 118 | + value={query} | |
| 119 | + /> | |
| 120 | + <Text className="tabular-nums text-sm text-foreground-muted"> | |
| 121 | + {filteredEntries.length === entries.length | |
| 122 | + ? `${String(entries.length)} notices` | |
| 123 | + : `${String(filteredEntries.length)} of ${String(entries.length)} notices`} | |
| 124 | + </Text> | |
| 125 | + </View> | |
| 126 | + } | |
| 127 | + renderItem={renderItem} | |
| 128 | + showsVerticalScrollIndicator={false} | |
| 129 | + /> | |
| 130 | + </View> | |
| 131 | + ); | |
| 132 | +} | |
| 133 | + | |
| 134 | +type LicenseDetailProps = StaticScreenProps<{ readonly entryKey: string }>; | |
| 135 | + | |
| 136 | +export function SettingsOpenSourceLicenseRouteScreen({ route }: LicenseDetailProps) { | |
| 137 | + const navigation = useNavigation(); | |
| 138 | + const insets = useSafeAreaInsets(); | |
| 139 | + const entry = findThirdPartyLicenseEntry( | |
| 140 | + MOBILE_THIRD_PARTY_LICENSES.entries, | |
| 141 | + route.params.entryKey, | |
| 142 | + ); | |
| 143 | + const sourceUrl = entry?.sourceUrl?.match(/^https?:\/\//) ? entry.sourceUrl : null; | |
| 144 | + | |
| 145 | + if (!entry) { | |
| 146 | + return ( | |
| 147 | + <View collapsable={false} className="flex-1 bg-sheet"> | |
| 148 | + {Platform.OS === "android" ? ( | |
| 149 | + <> | |
| 150 | + <NativeStackScreenOptions options={{ headerShown: false }} /> | |
| 151 | + <AndroidScreenHeader title="License notice" onBack={() => navigation.goBack()} /> | |
| 152 | + </> | |
| 153 | + ) : null} | |
| 154 | + <View className="flex-1 items-center justify-center px-6"> | |
| 155 | + <Text className="text-center text-base text-foreground-muted"> | |
| 156 | + This license notice is unavailable. | |
| 157 | + </Text> | |
| 158 | + </View> | |
| 159 | + </View> | |
| 160 | + ); | |
| 161 | + } | |
| 162 | + | |
| 163 | + return ( | |
| 164 | + <View collapsable={false} className="flex-1 bg-sheet"> | |
| 165 | + {Platform.OS === "android" ? ( | |
| 166 | + <> | |
| 167 | + <NativeStackScreenOptions options={{ headerShown: false }} /> | |
| 168 | + <AndroidScreenHeader title="License notice" onBack={() => navigation.goBack()} /> | |
| 169 | + </> | |
| 170 | + ) : null} | |
| 171 | + <ScrollView | |
| 172 | + className="flex-1" | |
| 173 | + contentInsetAdjustmentBehavior="automatic" | |
| 174 | + contentContainerClassName="gap-5 px-5 pt-4" | |
| 175 | + contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }} | |
| 176 | + showsVerticalScrollIndicator={false} | |
| 177 | + > | |
| 178 | + <View className="gap-2 px-1"> | |
| 179 | + <Text className="text-2xl font-t3-bold text-foreground">{entry.name}</Text> | |
| 180 | + <Text className="text-base leading-normal text-foreground-muted"> | |
| 181 | + {[entry.version, entry.license, formatLicenseBundles(entry.bundles)] | |
| 182 | + .filter((value): value is string => Boolean(value)) | |
| 183 | + .join(" · ")} | |
| 184 | + </Text> | |
| 185 | + {sourceUrl ? ( | |
| 186 | + <Pressable | |
| 187 | + accessibilityHint="Opens the project website" | |
| 188 | + accessibilityRole="link" | |
| 189 | + onPress={() => void Linking.openURL(sourceUrl)} | |
| 190 | + className="min-h-12 flex-row items-center gap-2 self-start py-2 active:opacity-60" | |
| 191 | + > | |
| 192 | + <Text className="font-t3-medium text-primary">Project source</Text> | |
| 193 | + <SymbolView | |
| 194 | + name="arrow.up.right" | |
| 195 | + size={16} | |
| 196 | + tintColorClassName={"accent-primary"} | |
| 197 | + type="monochrome" | |
| 198 | + weight="semibold" | |
| 199 | + /> | |
| 200 | + </Pressable> | |
| 201 | + ) : null} | |
| 202 | + </View> | |
| 203 | + | |
| 204 | + <View className="overflow-hidden rounded-[24px] border-continuous bg-card p-4"> | |
| 205 | + <Text selectable className="font-mono text-base leading-normal text-foreground"> | |
| 206 | + {entry.noticeText} | |
| 207 | + </Text> | |
| 208 | + </View> | |
| 209 | + </ScrollView> | |
| 210 | + </View> | |
| 211 | + ); | |
| 212 | +} | |
| @@ -670,6 +670,11 @@ function AppSettingsSection() { · 669 unmodified lines | ||
| 670 | 670 | return ( |
| 671 | 671 | <SettingsSection title="App"> |
| 672 | 672 | <SettingsRow icon="internaldrive" label="Client Storage" target="SettingsClientStorage" /> |
| 673 | + <SettingsRow | |
| 674 | + icon="doc.on.doc" | |
| 675 | + label="Open source licenses" | |
| 676 | + target="SettingsOpenSourceLicenses" | |
| 677 | + /> | |
| 673 | 678 | <SettingsRow icon="doc.text" label="Legal" fullScreenTarget="SettingsLegal" /> |
| 674 | 679 | {updateCheckAvailable ? ( |
| 675 | 680 | <Pressable |
| @@ -4,6 +4,7 @@ export type SettingsSheetTarget = · 3 unmodified lines | ||
| 4 | 4 | | "SettingsAppearance" |
| 5 | 5 | | "SettingsProjectGrouping" |
| 6 | 6 | | "SettingsClientStorage" |
| 7 | + | "SettingsOpenSourceLicenses" | |
| 7 | 8 | | "SettingsUsage"; |
| 8 | 9 | |
| 9 | 10 | export type SettingsLegalDocumentTarget = "SettingsLegal"; |
| @@ -0,0 +1,4 @@ | ||
| 1 | +import generatedManifest from "@t3tools/mobile-third-party-licenses"; | |
| 2 | +import { decodeThirdPartyLicenseManifest } from "@t3tools/shared/thirdPartyLicenses"; | |
| 3 | + | |
| 4 | +export const MOBILE_THIRD_PARTY_LICENSES = decodeThirdPartyLicenseManifest(generatedManifest); | |
| @@ -0,0 +1,4 @@ | ||
| 1 | +declare module "@t3tools/mobile-third-party-licenses" { | |
| 2 | + const manifest: unknown; | |
| 3 | + export default manifest; | |
| 4 | +} | |
| @@ -1,11 +1,24 @@ | ||
| 1 | −# Third-Party Notices | |
| 1 | +vscode-icons | |
| 2 | 2 | |
| 3 | −## vscode-icons | |
| 4 | − | |
| 5 | −The custom file icon symbols in `src/pierre-icons.ts` are adapted from the | |
| 6 | −[`vscode-icons`](https://github.com/vscode-icons/vscode-icons) project. | |
| 3 | +The custom file icon symbols bundled with T3 Code are adapted from the | |
| 4 | +vscode-icons project: https://github.com/vscode-icons/vscode-icons | |
| 7 | 5 | |
| 8 | 6 | Copyright (c) 2016 Roberto Huertas |
| 9 | 7 | |
| 10 | −Licensed under the MIT License. The full license text is available in the | |
| 11 | −upstream repository: <https://github.com/vscode-icons/vscode-icons/blob/master/LICENSE>. | |
| 8 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 9 | +of this software and associated documentation files (the "Software"), to deal | |
| 10 | +in the Software without restriction, including without limitation the rights | |
| 11 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 12 | +copies of the Software, and to permit persons to whom the Software is | |
| 13 | +furnished to do so, subject to the following conditions: | |
| 14 | + | |
| 15 | +The above copyright notice and this permission notice shall be included in all | |
| 16 | +copies or substantial portions of the Software. | |
| 17 | + | |
| 18 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 19 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 20 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 21 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 22 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 23 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 24 | +SOFTWARE. | |
| @@ -0,0 +1,25 @@ | ||
| 1 | +BSD 2-Clause License | |
| 2 | + | |
| 3 | +Copyright (c) 2026 Samuel Attard and Electron contributors | |
| 4 | +All rights reserved. | |
| 5 | + | |
| 6 | +Redistribution and use in source and binary forms, with or without | |
| 7 | +modification, are permitted provided that the following conditions are met: | |
| 8 | + | |
| 9 | +1. Redistributions of source code must retain the above copyright notice, | |
| 10 | + this list of conditions and the following disclaimer. | |
| 11 | + | |
| 12 | +2. Redistributions in binary form must reproduce the above copyright notice, | |
| 13 | + this list of conditions and the following disclaimer in the documentation | |
| 14 | + and/or other materials provided with the distribution. | |
| 15 | + | |
| 16 | +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| 17 | +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 18 | +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| 19 | +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| 20 | +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| 21 | +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| 22 | +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| 23 | +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| 24 | +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| @@ -0,0 +1,24 @@ | ||
| 1 | +Copyright (c) 2013, Nick Fitzgerald | |
| 2 | + | |
| 3 | +All rights reserved. | |
| 4 | + | |
| 5 | +Redistribution and use in source and binary forms, with or without modification, | |
| 6 | +are permitted provided that the following conditions are met: | |
| 7 | + | |
| 8 | +* Redistributions of source code must retain the above copyright notice, this | |
| 9 | + list of conditions and the following disclaimer. | |
| 10 | + | |
| 11 | +* Redistributions in binary form must reproduce the above copyright notice, this | |
| 12 | + list of conditions and the following disclaimer in the documentation and/or | |
| 13 | + other materials provided with the distribution. | |
| 14 | + | |
| 15 | +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
| 16 | +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
| 17 | +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| 18 | +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| 19 | +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
| 20 | +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
| 21 | +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
| 22 | +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 23 | +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
| 24 | +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +(MIT) | |
| 2 | + | |
| 3 | +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| 6 | +this software and associated documentation files (the "Software"), to deal in | |
| 7 | +the Software without restriction, including without limitation the rights to | |
| 8 | +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
| 9 | +of the Software, and to permit persons to whom the Software is furnished to do | |
| 10 | +so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2017-2021 Luke Childs | |
| 4 | +Copyright (c) 2021-2022 Jared Wray | |
| 5 | + | |
| 6 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 7 | +of this software and associated documentation files (the "Software"), to deal | |
| 8 | +in the Software without restriction, including without limitation the rights | |
| 9 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 10 | +copies of the Software, and to permit persons to whom the Software is | |
| 11 | +furnished to do so, subject to the following conditions: | |
| 12 | + | |
| 13 | +The above copyright notice and this permission notice shall be included in all | |
| 14 | +copies or substantial portions of the Software. | |
| 15 | + | |
| 16 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 17 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 18 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 19 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 20 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 21 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 22 | +SOFTWARE. | |
| @@ -0,0 +1,176 @@ | ||
| 1 | +Apache License | |
| 2 | + Version 2.0, January 2004 | |
| 3 | + http://www.apache.org/licenses/ | |
| 4 | + | |
| 5 | + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | + | |
| 7 | + 1. Definitions. | |
| 8 | + | |
| 9 | + "License" shall mean the terms and conditions for use, reproduction, | |
| 10 | + and distribution as defined by Sections 1 through 9 of this document. | |
| 11 | + | |
| 12 | + "Licensor" shall mean the copyright owner or entity authorized by | |
| 13 | + the copyright owner that is granting the License. | |
| 14 | + | |
| 15 | + "Legal Entity" shall mean the union of the acting entity and all | |
| 16 | + other entities that control, are controlled by, or are under common | |
| 17 | + control with that entity. For the purposes of this definition, | |
| 18 | + "control" means (i) the power, direct or indirect, to cause the | |
| 19 | + direction or management of such entity, whether by contract or | |
| 20 | + otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 21 | + outstanding shares, or (iii) beneficial ownership of such entity. | |
| 22 | + | |
| 23 | + "You" (or "Your") shall mean an individual or Legal Entity | |
| 24 | + exercising permissions granted by this License. | |
| 25 | + | |
| 26 | + "Source" form shall mean the preferred form for making modifications, | |
| 27 | + including but not limited to software source code, documentation | |
| 28 | + source, and configuration files. | |
| 29 | + | |
| 30 | + "Object" form shall mean any form resulting from mechanical | |
| 31 | + transformation or translation of a Source form, including but | |
| 32 | + not limited to compiled object code, generated documentation, | |
| 33 | + and conversions to other media types. | |
| 34 | + | |
| 35 | + "Work" shall mean the work of authorship, whether in Source or | |
| 36 | + Object form, made available under the License, as indicated by a | |
| 37 | + copyright notice that is included in or attached to the work | |
| 38 | + (an example is provided in the Appendix below). | |
| 39 | + | |
| 40 | + "Derivative Works" shall mean any work, whether in Source or Object | |
| 41 | + form, that is based on (or derived from) the Work and for which the | |
| 42 | + editorial revisions, annotations, elaborations, or other modifications | |
| 43 | + represent, as a whole, an original work of authorship. For the purposes | |
| 44 | + of this License, Derivative Works shall not include works that remain | |
| 45 | + separable from, or merely link (or bind by name) to the interfaces of, | |
| 46 | + the Work and Derivative Works thereof. | |
| 47 | + | |
| 48 | + "Contribution" shall mean any work of authorship, including | |
| 49 | + the original version of the Work and any modifications or additions | |
| 50 | + to that Work or Derivative Works thereof, that is intentionally | |
| 51 | + submitted to Licensor for inclusion in the Work by the copyright owner | |
| 52 | + or by an individual or Legal Entity authorized to submit on behalf of | |
| 53 | + the copyright owner. For the purposes of this definition, "submitted" | |
| 54 | + means any form of electronic, verbal, or written communication sent | |
| 55 | + to the Licensor or its representatives, including but not limited to | |
| 56 | + communication on electronic mailing lists, source code control systems, | |
| 57 | + and issue tracking systems that are managed by, or on behalf of, the | |
| 58 | + Licensor for the purpose of discussing and improving the Work, but | |
| 59 | + excluding communication that is conspicuously marked or otherwise | |
| 60 | + designated in writing by the copyright owner as "Not a Contribution." | |
| 61 | + | |
| 62 | + "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 63 | + on behalf of whom a Contribution has been received by Licensor and | |
| 64 | + subsequently incorporated within the Work. | |
| 65 | + | |
| 66 | + 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 67 | + this License, each Contributor hereby grants to You a perpetual, | |
| 68 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 69 | + copyright license to reproduce, prepare Derivative Works of, | |
| 70 | + publicly display, publicly perform, sublicense, and distribute the | |
| 71 | + Work and such Derivative Works in Source or Object form. | |
| 72 | + | |
| 73 | + 3. Grant of Patent License. Subject to the terms and conditions of | |
| 74 | + this License, each Contributor hereby grants to You a perpetual, | |
| 75 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 76 | + (except as stated in this section) patent license to make, have made, | |
| 77 | + use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 78 | + where such license applies only to those patent claims licensable | |
| 79 | + by such Contributor that are necessarily infringed by their | |
| 80 | + Contribution(s) alone or by combination of their Contribution(s) | |
| 81 | + with the Work to which such Contribution(s) was submitted. If You | |
| 82 | + institute patent litigation against any entity (including a | |
| 83 | + cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 84 | + or a Contribution incorporated within the Work constitutes direct | |
| 85 | + or contributory patent infringement, then any patent licenses | |
| 86 | + granted to You under this License for that Work shall terminate | |
| 87 | + as of the date such litigation is filed. | |
| 88 | + | |
| 89 | + 4. Redistribution. You may reproduce and distribute copies of the | |
| 90 | + Work or Derivative Works thereof in any medium, with or without | |
| 91 | + modifications, and in Source or Object form, provided that You | |
| 92 | + meet the following conditions: | |
| 93 | + | |
| 94 | + (a) You must give any other recipients of the Work or | |
| 95 | + Derivative Works a copy of this License; and | |
| 96 | + | |
| 97 | + (b) You must cause any modified files to carry prominent notices | |
| 98 | + stating that You changed the files; and | |
| 99 | + | |
| 100 | + (c) You must retain, in the Source form of any Derivative Works | |
| 101 | + that You distribute, all copyright, patent, trademark, and | |
| 102 | + attribution notices from the Source form of the Work, | |
| 103 | + excluding those notices that do not pertain to any part of | |
| 104 | + the Derivative Works; and | |
| 105 | + | |
| 106 | + (d) If the Work includes a "NOTICE" text file as part of its | |
| 107 | + distribution, then any Derivative Works that You distribute must | |
| 108 | + include a readable copy of the attribution notices contained | |
| 109 | + within such NOTICE file, excluding those notices that do not | |
| 110 | + pertain to any part of the Derivative Works, in at least one | |
| 111 | + of the following places: within a NOTICE text file distributed | |
| 112 | + as part of the Derivative Works; within the Source form or | |
| 113 | + documentation, if provided along with the Derivative Works; or, | |
| 114 | + within a display generated by the Derivative Works, if and | |
| 115 | + wherever such third-party notices normally appear. The contents | |
| 116 | + of the NOTICE file are for informational purposes only and | |
| 117 | + do not modify the License. You may add Your own attribution | |
| 118 | + notices within Derivative Works that You distribute, alongside | |
| 119 | + or as an addendum to the NOTICE text from the Work, provided | |
| 120 | + that such additional attribution notices cannot be construed | |
| 121 | + as modifying the License. | |
| 122 | + | |
| 123 | + You may add Your own copyright statement to Your modifications and | |
| 124 | + may provide additional or different license terms and conditions | |
| 125 | + for use, reproduction, or distribution of Your modifications, or | |
| 126 | + for any such Derivative Works as a whole, provided Your use, | |
| 127 | + reproduction, and distribution of the Work otherwise complies with | |
| 128 | + the conditions stated in this License. | |
| 129 | + | |
| 130 | + 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 131 | + any Contribution intentionally submitted for inclusion in the Work | |
| 132 | + by You to the Licensor shall be under the terms and conditions of | |
| 133 | + this License, without any additional terms or conditions. | |
| 134 | + Notwithstanding the above, nothing herein shall supersede or modify | |
| 135 | + the terms of any separate license agreement you may have executed | |
| 136 | + with Licensor regarding such Contributions. | |
| 137 | + | |
| 138 | + 6. Trademarks. This License does not grant permission to use the trade | |
| 139 | + names, trademarks, service marks, or product names of the Licensor, | |
| 140 | + except as required for reasonable and customary use in describing the | |
| 141 | + origin of the Work and reproducing the content of the NOTICE file. | |
| 142 | + | |
| 143 | + 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 144 | + agreed to in writing, Licensor provides the Work (and each | |
| 145 | + Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 146 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 147 | + implied, including, without limitation, any warranties or conditions | |
| 148 | + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 149 | + PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 150 | + appropriateness of using or redistributing the Work and assume any | |
| 151 | + risks associated with Your exercise of permissions under this License. | |
| 152 | + | |
| 153 | + 8. Limitation of Liability. In no event and under no legal theory, | |
| 154 | + whether in tort (including negligence), contract, or otherwise, | |
| 155 | + unless required by applicable law (such as deliberate and grossly | |
| 156 | + negligent acts) or agreed to in writing, shall any Contributor be | |
| 157 | + liable to You for damages, including any direct, indirect, special, | |
| 158 | + incidental, or consequential damages of any character arising as a | |
| 159 | + result of this License or out of the use or inability to use the | |
| 160 | + Work (including but not limited to damages for loss of goodwill, | |
| 161 | + work stoppage, computer failure or malfunction, or any and all | |
| 162 | + other commercial damages or losses), even if such Contributor | |
| 163 | + has been advised of the possibility of such damages. | |
| 164 | + | |
| 165 | + 9. Accepting Warranty or Additional Liability. While redistributing | |
| 166 | + the Work or Derivative Works thereof, You may choose to offer, | |
| 167 | + and charge a fee for, acceptance of support, warranty, indemnity, | |
| 168 | + or other liability obligations and/or rights consistent with this | |
| 169 | + License. However, in accepting such obligations, You may act only | |
| 170 | + on Your own behalf and on Your sole responsibility, not on behalf | |
| 171 | + of any other Contributor, and only if You agree to indemnify, | |
| 172 | + defend, and hold each Contributor harmless for any liability | |
| 173 | + incurred by, or claims asserted against, such Contributor by reason | |
| 174 | + of your accepting any such warranty or additional liability. | |
| 175 | + | |
| 176 | + END OF TERMS AND CONDITIONS | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) Vladimir Krivosheev | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,19 @@ | ||
| 1 | +Copyright (c) 2010-2016 Rasmus Andersson <https://rsms.me/> | |
| 2 | + | |
| 3 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 4 | +of this software and associated documentation files (the "Software"), to deal | |
| 5 | +in the Software without restriction, including without limitation the rights | |
| 6 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 7 | +copies of the Software, and to permit persons to whom the Software is | |
| 8 | +furnished to do so, subject to the following conditions: | |
| 9 | + | |
| 10 | +The above copyright notice and this permission notice shall be included in | |
| 11 | +all copies or substantial portions of the Software. | |
| 12 | + | |
| 13 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 14 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 15 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 16 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 17 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 18 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 19 | +THE SOFTWARE. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2020 Kris Zyp | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,189 @@ | ||
| 1 | + Apache License | |
| 2 | + Version 2.0, January 2004 | |
| 3 | + http://www.apache.org/licenses/ | |
| 4 | + | |
| 5 | +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 6 | + | |
| 7 | +1. Definitions. | |
| 8 | + | |
| 9 | + "License" shall mean the terms and conditions for use, reproduction, and | |
| 10 | + distribution as defined by Sections 1 through 9 of this document. | |
| 11 | + | |
| 12 | + "Licensor" shall mean the copyright owner or entity authorized by the | |
| 13 | + copyright owner that is granting the License. | |
| 14 | + | |
| 15 | + "Legal Entity" shall mean the union of the acting entity and all other | |
| 16 | + entities that control, are controlled by, or are under common control with | |
| 17 | + that entity. For the purposes of this definition, "control" means (i) the | |
| 18 | + power, direct or indirect, to cause the direction or management of such | |
| 19 | + entity, whether by contract or otherwise, or (ii) ownership of fifty percent | |
| 20 | + (50%) or more of the outstanding shares, or (iii) beneficial ownership of | |
| 21 | + such entity. | |
| 22 | + | |
| 23 | + "You" (or "Your") shall mean an individual or Legal Entity exercising | |
| 24 | + permissions granted by this License. | |
| 25 | + | |
| 26 | + "Source" form shall mean the preferred form for making modifications, | |
| 27 | + including but not limited to software source code, documentation source, and | |
| 28 | + configuration files. | |
| 29 | + | |
| 30 | + "Object" form shall mean any form resulting from mechanical transformation | |
| 31 | + or translation of a Source form, including but not limited to compiled | |
| 32 | + object code, generated documentation, and conversions to other media types. | |
| 33 | + | |
| 34 | + "Work" shall mean the work of authorship, whether in Source or Object form, | |
| 35 | + made available under the License, as indicated by a copyright notice that is | |
| 36 | + included in or attached to the work (an example is provided in the Appendix | |
| 37 | + below). | |
| 38 | + | |
| 39 | + "Derivative Works" shall mean any work, whether in Source or Object form, | |
| 40 | + that is based on (or derived from) the Work and for which the editorial | |
| 41 | + revisions, annotations, elaborations, or other modifications represent, as a | |
| 42 | + whole, an original work of authorship. For the purposes of this License, | |
| 43 | + Derivative Works shall not include works that remain separable from, or | |
| 44 | + merely link (or bind by name) to the interfaces of, the Work and Derivative | |
| 45 | + Works thereof. | |
| 46 | + | |
| 47 | + "Contribution" shall mean any work of authorship, including the original | |
| 48 | + version of the Work and any modifications or additions to that Work or | |
| 49 | + Derivative Works thereof, that is intentionally submitted to Licensor for | |
| 50 | + inclusion in the Work by the copyright owner or by an individual or Legal | |
| 51 | + Entity authorized to submit on behalf of the copyright owner. For the | |
| 52 | + purposes of this definition, "submitted" means any form of electronic, | |
| 53 | + verbal, or written communication sent to the Licensor or its | |
| 54 | + representatives, including but not limited to communication on electronic | |
| 55 | + mailing lists, source code control systems, and issue tracking systems that | |
| 56 | + are managed by, or on behalf of, the Licensor for the purpose of discussing | |
| 57 | + and improving the Work, but excluding communication that is conspicuously | |
| 58 | + marked or otherwise designated in writing by the copyright owner as "Not a | |
| 59 | + Contribution." | |
| 60 | + | |
| 61 | + "Contributor" shall mean Licensor and any individual or Legal Entity on | |
| 62 | + behalf of whom a Contribution has been received by Licensor and subsequently | |
| 63 | + incorporated within the Work. | |
| 64 | + | |
| 65 | +2. Grant of Copyright License. Subject to the terms and conditions of this | |
| 66 | + License, each Contributor hereby grants to You a perpetual, worldwide, | |
| 67 | + non-exclusive, no-charge, royalty-free, irrevocable copyright license to | |
| 68 | + reproduce, prepare Derivative Works of, publicly display, publicly perform, | |
| 69 | + sublicense, and distribute the Work and such Derivative Works in Source or | |
| 70 | + Object form. | |
| 71 | + | |
| 72 | +3. Grant of Patent License. Subject to the terms and conditions of this | |
| 73 | + License, each Contributor hereby grants to You a perpetual, worldwide, | |
| 74 | + non-exclusive, no-charge, royalty-free, irrevocable (except as stated in | |
| 75 | + this section) patent license to make, have made, use, offer to sell, sell, | |
| 76 | + import, and otherwise transfer the Work, where such license applies only to | |
| 77 | + those patent claims licensable by such Contributor that are necessarily | |
| 78 | + infringed by their Contribution(s) alone or by combination of their | |
| 79 | + Contribution(s) with the Work to which such Contribution(s) was submitted. | |
| 80 | + If You institute patent litigation against any entity (including a | |
| 81 | + cross-claim or counterclaim in a lawsuit) alleging that the Work or a | |
| 82 | + Contribution incorporated within the Work constitutes direct or contributory | |
| 83 | + patent infringement, then any patent licenses granted to You under this | |
| 84 | + License for that Work shall terminate as of the date such litigation is | |
| 85 | + filed. | |
| 86 | + | |
| 87 | +4. Redistribution. You may reproduce and distribute copies of the Work or | |
| 88 | + Derivative Works thereof in any medium, with or without modifications, and | |
| 89 | + in Source or Object form, provided that You meet the following conditions: | |
| 90 | + | |
| 91 | + (a) You must give any other recipients of the Work or Derivative Works a | |
| 92 | + copy of this License; and | |
| 93 | + | |
| 94 | + (b) You must cause any modified files to carry prominent notices stating | |
| 95 | + that You changed the files; and | |
| 96 | + | |
| 97 | + (c) You must retain, in the Source form of any Derivative Works that You | |
| 98 | + distribute, all copyright, patent, trademark, and attribution notices from | |
| 99 | + the Source form of the Work, excluding those notices that do not pertain to | |
| 100 | + any part of the Derivative Works; and | |
| 101 | + | |
| 102 | + (d) If the Work includes a "NOTICE" text file as part of its distribution, | |
| 103 | + then any Derivative Works that You distribute must include a readable copy | |
| 104 | + of the attribution notices contained within such NOTICE file, excluding | |
| 105 | + those notices that do not pertain to any part of the Derivative Works, in at | |
| 106 | + least one of the following places: within a NOTICE text file distributed as | |
| 107 | + part of the Derivative Works; within the Source form or documentation, if | |
| 108 | + provided along with the Derivative Works; or, within a display generated by | |
| 109 | + the Derivative Works, if and wherever such third-party notices normally | |
| 110 | + appear. The contents of the NOTICE file are for informational purposes only | |
| 111 | + and do not modify the License. You may add Your own attribution notices | |
| 112 | + within Derivative Works that You distribute, alongside or as an addendum to | |
| 113 | + the NOTICE text from the Work, provided that such additional attribution | |
| 114 | + notices cannot be construed as modifying the License. | |
| 115 | + | |
| 116 | + You may add Your own copyright statement to Your modifications and may | |
| 117 | + provide additional or different license terms and conditions for use, | |
| 118 | + reproduction, or distribution of Your modifications, or for any such | |
| 119 | + Derivative Works as a whole, provided Your use, reproduction, and | |
| 120 | + distribution of the Work otherwise complies with the conditions stated in | |
| 121 | + this License. | |
| 122 | + | |
| 123 | +5. Submission of Contributions. Unless You explicitly state otherwise, any | |
| 124 | + Contribution intentionally submitted for inclusion in the Work by You to the | |
| 125 | + Licensor shall be under the terms and conditions of this License, without | |
| 126 | + any additional terms or conditions. Notwithstanding the above, nothing | |
| 127 | + herein shall supersede or modify the terms of any separate license agreement | |
| 128 | + you may have executed with Licensor regarding such Contributions. | |
| 129 | + | |
| 130 | +6. Trademarks. This License does not grant permission to use the trade names, | |
| 131 | + trademarks, service marks, or product names of the Licensor, except as | |
| 132 | + required for reasonable and customary use in describing the origin of the | |
| 133 | + Work and reproducing the content of the NOTICE file. | |
| 134 | + | |
| 135 | +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in | |
| 136 | + writing, Licensor provides the Work (and each Contributor provides its | |
| 137 | + Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| 138 | + KIND, either express or implied, including, without limitation, any | |
| 139 | + warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or | |
| 140 | + FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining | |
| 141 | + the appropriateness of using or redistributing the Work and assume any risks | |
| 142 | + associated with Your exercise of permissions under this License. | |
| 143 | + | |
| 144 | +8. Limitation of Liability. In no event and under no legal theory, whether in | |
| 145 | + tort (including negligence), contract, or otherwise, unless required by | |
| 146 | + applicable law (such as deliberate and grossly negligent acts) or agreed to | |
| 147 | + in writing, shall any Contributor be liable to You for damages, including | |
| 148 | + any direct, indirect, special, incidental, or consequential damages of any | |
| 149 | + character arising as a result of this License or out of the use or inability | |
| 150 | + to use the Work (including but not limited to damages for loss of goodwill, | |
| 151 | + work stoppage, computer failure or malfunction, or any and all other | |
| 152 | + commercial damages or losses), even if such Contributor has been advised of | |
| 153 | + the possibility of such damages. | |
| 154 | + | |
| 155 | +9. Accepting Warranty or Additional Liability. While redistributing the Work or | |
| 156 | + Derivative Works thereof, You may choose to offer, and charge a fee for, | |
| 157 | + acceptance of support, warranty, indemnity, or other liability obligations | |
| 158 | + and/or rights consistent with this License. However, in accepting such | |
| 159 | + obligations, You may act only on Your own behalf and on Your sole | |
| 160 | + responsibility, not on behalf of any other Contributor, and only if You | |
| 161 | + agree to indemnify, defend, and hold each Contributor harmless for any | |
| 162 | + liability incurred by, or claims asserted against, such Contributor by | |
| 163 | + reason of your accepting any such warranty or additional liability. | |
| 164 | + | |
| 165 | +END OF TERMS AND CONDITIONS | |
| 166 | + | |
| 167 | +APPENDIX: How to apply the Apache License to your work. | |
| 168 | + | |
| 169 | + To apply the Apache License to your work, attach the following | |
| 170 | + boilerplate notice, with the fields enclosed by brackets "[]" | |
| 171 | + replaced with your own identifying information. (Don't include | |
| 172 | + the brackets!) The text should be enclosed in the appropriate | |
| 173 | + comment syntax for the file format. We also recommend that a | |
| 174 | + file or class name and description of purpose be included on the | |
| 175 | + same "printed page" as the copyright notice for easier | |
| 176 | + identification within third-party archives. | |
| 177 | + | |
| 178 | +Copyright 2025 Pierre Computer Company | |
| 179 | + | |
| 180 | +Licensed under the Apache License, Version 2.0 (the "License"); you may not use | |
| 181 | +this file except in compliance with the License. You may obtain a copy of the | |
| 182 | +License at | |
| 183 | + | |
| 184 | + http://www.apache.org/licenses/LICENSE-2.0 | |
| 185 | + | |
| 186 | +Unless required by applicable law or agreed to in writing, software distributed | |
| 187 | +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR | |
| 188 | +CONDITIONS OF ANY KIND, either express or implied. See the License for the | |
| 189 | +specific language governing permissions and limitations under the License. | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2025 Aiden Bai | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | |
| @@ -0,0 +1,9 @@ | ||
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
| 6 | + | |
| 7 | +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
| 8 | + | |
| 9 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| @@ -0,0 +1,205 @@ | ||
| 1 | +import { ChevronRightIcon, ExternalLinkIcon, SearchIcon } from "lucide-react"; | |
| 2 | +import { useCallback, useEffect, useMemo, useState } from "react"; | |
| 3 | + | |
| 4 | +import { | |
| 5 | + decodeThirdPartyLicenseManifest, | |
| 6 | + filterThirdPartyLicenseEntries, | |
| 7 | + formatLicenseBundles, | |
| 8 | + type ThirdPartyLicenseEntry, | |
| 9 | + type ThirdPartyLicenseManifest, | |
| 10 | +} from "@t3tools/shared/thirdPartyLicenses"; | |
| 11 | + | |
| 12 | +import { cn } from "../../lib/utils"; | |
| 13 | +import { Button } from "../ui/button"; | |
| 14 | +import { Input } from "../ui/input"; | |
| 15 | +import { SettingsPageContainer } from "./settingsLayout"; | |
| 16 | + | |
| 17 | +type LicenseManifestState = | |
| 18 | + | { readonly status: "loading" } | |
| 19 | + | { readonly status: "error"; readonly message: string } | |
| 20 | + | { readonly status: "ready"; readonly manifest: ThirdPartyLicenseManifest }; | |
| 21 | + | |
| 22 | +async function loadLicenseManifest(signal: AbortSignal): Promise<ThirdPartyLicenseManifest> { | |
| 23 | + const response = await fetch( | |
| 24 | + `${import.meta.env.BASE_URL.replace(/\/$/, "")}/third-party-licenses.json`, | |
| 25 | + { signal }, | |
| 26 | + ); | |
| 27 | + if (!response.ok) { | |
| 28 | + throw new Error(`The license manifest request failed with status ${String(response.status)}.`); | |
| 29 | + } | |
| 30 | + return decodeThirdPartyLicenseManifest((await response.json()) as unknown); | |
| 31 | +} | |
| 32 | + | |
| 33 | +function LicenseNoticeRow({ entry }: { readonly entry: ThirdPartyLicenseEntry }) { | |
| 34 | + const [open, setOpen] = useState(false); | |
| 35 | + const panelId = `license-notice-${encodeURIComponent(`${entry.kind}-${entry.name}-${entry.version ?? "custom"}`)}`; | |
| 36 | + const versionLabel = entry.version ? ` ${entry.version}` : ""; | |
| 37 | + | |
| 38 | + return ( | |
| 39 | + <article> | |
| 40 | + <button | |
| 41 | + type="button" | |
| 42 | + aria-controls={panelId} | |
| 43 | + aria-expanded={open} | |
| 44 | + className="cursor-pointer flex min-h-14 w-full items-start gap-3 px-3 py-3 text-left hover:bg-foreground/4 sm:px-4" | |
| 45 | + onClick={() => setOpen((value) => !value)} | |
| 46 | + > | |
| 47 | + <ChevronRightIcon | |
| 48 | + aria-hidden | |
| 49 | + className={cn( | |
| 50 | + "mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform duration-150", | |
| 51 | + open && "rotate-90", | |
| 52 | + )} | |
| 53 | + /> | |
| 54 | + <span className="min-w-0 flex-1"> | |
| 55 | + <span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1"> | |
| 56 | + <span className="min-w-0 break-words font-medium text-foreground">{entry.name}</span> | |
| 57 | + {entry.version ? ( | |
| 58 | + <code className="font-medium text-muted-foreground">{entry.version}</code> | |
| 59 | + ) : null} | |
| 60 | + </span> | |
| 61 | + <span className="text-muted-foreground"> | |
| 62 | + {entry.license} · {formatLicenseBundles(entry.bundles)} | |
| 63 | + </span> | |
| 64 | + </span> | |
| 65 | + </button> | |
| 66 | + {open ? ( | |
| 67 | + <div | |
| 68 | + id={panelId} | |
| 69 | + className="border-t border-foreground/10 bg-foreground/2 px-3 py-4 sm:px-4" | |
| 70 | + > | |
| 71 | + <div className="flex flex-col gap-3"> | |
| 72 | + <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 73 | + <h2 className="text-base font-medium text-foreground sm:text-sm"> | |
| 74 | + {entry.name} | |
| 75 | + {versionLabel} | |
| 76 | + </h2> | |
| 77 | + {entry.sourceUrl ? ( | |
| 78 | + <a | |
| 79 | + className="inline-flex min-h-8 items-center gap-1.5 rounded-md px-2 text-base font-medium text-muted-foreground hover:bg-foreground/5 hover:text-foreground sm:text-sm" | |
| 80 | + href={entry.sourceUrl} | |
| 81 | + rel="noreferrer noopener" | |
| 82 | + target="_blank" | |
| 83 | + > | |
| 84 | + Project source | |
| 85 | + <ExternalLinkIcon aria-hidden className="size-4 shrink-0" /> | |
| 86 | + </a> | |
| 87 | + ) : null} | |
| 88 | + </div> | |
| 89 | + <pre className="max-w-full whitespace-pre-wrap break-words font-mono text-base/7 text-foreground/85 sm:text-sm/6"> | |
| 90 | + {entry.noticeText} | |
| 91 | + </pre> | |
| 92 | + </div> | |
| 93 | + </div> | |
| 94 | + ) : null} | |
| 95 | + </article> | |
| 96 | + ); | |
| 97 | +} | |
| 98 | + | |
| 99 | +function LicenseManifestError({ message, onRetry }: { message: string; onRetry: () => void }) { | |
| 100 | + return ( | |
| 101 | + <div className="flex flex-col items-start gap-3 border-y border-foreground/10 px-3 py-5 sm:px-4"> | |
| 102 | + <div className="flex flex-col gap-1"> | |
| 103 | + <h2 className="text-base font-medium text-foreground sm:text-sm"> | |
| 104 | + Open-source notices are unavailable | |
| 105 | + </h2> | |
| 106 | + <p className="max-w-[70ch] text-pretty text-base/7 text-muted-foreground sm:text-sm/6"> | |
| 107 | + {message} | |
| 108 | + </p> | |
| 109 | + </div> | |
| 110 | + <Button type="button" size="xs" variant="outline" onClick={onRetry}> | |
| 111 | + Try again | |
| 112 | + </Button> | |
| 113 | + </div> | |
| 114 | + ); | |
| 115 | +} | |
| 116 | + | |
| 117 | +export function OpenSourceLicensesPanel() { | |
| 118 | + const [state, setState] = useState<LicenseManifestState>({ status: "loading" }); | |
| 119 | + const [query, setQuery] = useState(""); | |
| 120 | + const [requestVersion, setRequestVersion] = useState(0); | |
| 121 | + | |
| 122 | + useEffect(() => { | |
| 123 | + const controller = new AbortController(); | |
| 124 | + setState({ status: "loading" }); | |
| 125 | + void loadLicenseManifest(controller.signal).then( | |
| 126 | + (manifest) => setState({ status: "ready", manifest }), | |
| 127 | + (error: unknown) => { | |
| 128 | + if (controller.signal.aborted) return; | |
| 129 | + setState({ | |
| 130 | + status: "error", | |
| 131 | + message: error instanceof Error ? error.message : "The license manifest could not load.", | |
| 132 | + }); | |
| 133 | + }, | |
| 134 | + ); | |
| 135 | + return () => controller.abort(); | |
| 136 | + }, [requestVersion]); | |
| 137 | + | |
| 138 | + const entries = state.status === "ready" ? state.manifest.entries : []; | |
| 139 | + const filteredEntries = useMemo( | |
| 140 | + () => filterThirdPartyLicenseEntries(entries, query), | |
| 141 | + [entries, query], | |
| 142 | + ); | |
| 143 | + const retry = useCallback(() => setRequestVersion((value) => value + 1), []); | |
| 144 | + | |
| 145 | + return ( | |
| 146 | + <SettingsPageContainer width="wide" className="gap-6"> | |
| 147 | + <header className="flex flex-col gap-2 px-3 sm:px-4"> | |
| 148 | + <h1 className="text-balance text-2xl font-semibold tracking-tight text-foreground"> | |
| 149 | + Open source licenses | |
| 150 | + </h1> | |
| 151 | + <p className="max-w-[70ch] text-pretty text-base/7 text-muted-foreground sm:text-sm/6"> | |
| 152 | + Third-party notices for dependencies and assets included in T3 Code. | |
| 153 | + </p> | |
| 154 | + </header> | |
| 155 | + | |
| 156 | + {state.status === "ready" ? ( | |
| 157 | + <div className="flex flex-col gap-3"> | |
| 158 | + <div className="flex flex-col gap-2 px-3 sm:flex-row sm:items-center sm:justify-between sm:px-4"> | |
| 159 | + <label className="relative min-w-0 flex-1 sm:max-w-sm"> | |
| 160 | + <SearchIcon | |
| 161 | + aria-hidden | |
| 162 | + className="pointer-events-none absolute top-1/2 left-2.5 z-1 size-4 shrink-0 -translate-y-1/2 text-muted-foreground" | |
| 163 | + /> | |
| 164 | + <Input | |
| 165 | + nativeInput | |
| 166 | + type="search" | |
| 167 | + value={query} | |
| 168 | + onChange={(event) => setQuery(event.currentTarget.value)} | |
| 169 | + placeholder="Search packages" | |
| 170 | + aria-label="Search open-source licenses" | |
| 171 | + className="[&_[data-slot=input]]:pl-8" | |
| 172 | + /> | |
| 173 | + </label> | |
| 174 | + <p className="text-base text-muted-foreground tabular-nums sm:text-sm"> | |
| 175 | + {filteredEntries.length === entries.length | |
| 176 | + ? `${String(entries.length)} notices` | |
| 177 | + : `${String(filteredEntries.length)} of ${String(entries.length)} notices`} | |
| 178 | + </p> | |
| 179 | + </div> | |
| 180 | + | |
| 181 | + <div className="overflow-hidden rounded-xl border border-foreground/10 divide-y divide-foreground/10 text-base sm:text-sm"> | |
| 182 | + {filteredEntries.length > 0 ? ( | |
| 183 | + filteredEntries.map((entry) => ( | |
| 184 | + <LicenseNoticeRow | |
| 185 | + key={`${entry.kind}:${entry.name}:${entry.version ?? "custom"}`} | |
| 186 | + entry={entry} | |
| 187 | + /> | |
| 188 | + )) | |
| 189 | + ) : ( | |
| 190 | + <p className="px-3 py-8 text-center text-base/7 text-muted-foreground sm:px-4 sm:text-sm/6"> | |
| 191 | + No licenses match that search. | |
| 192 | + </p> | |
| 193 | + )} | |
| 194 | + </div> | |
| 195 | + </div> | |
| 196 | + ) : state.status === "error" ? ( | |
| 197 | + <LicenseManifestError message={state.message} onRetry={retry} /> | |
| 198 | + ) : ( | |
| 199 | + <p className="border-y border-foreground/10 px-3 py-5 text-base/7 text-muted-foreground sm:px-4 sm:text-sm/6"> | |
| 200 | + Loading open-source notices… | |
| 201 | + </p> | |
| 202 | + )} | |
| 203 | + </SettingsPageContainer> | |
| 204 | + ); | |
| 205 | +} | |
| @@ -8,6 +8,7 @@ import { SETTINGS_SECTION_LABELS } from "./settingsSearch"; · 7 unmodified lines | ||
| 8 | 8 | const SETTINGS_BREADCRUMB_LABELS: Readonly<Record<string, string>> = { |
| 9 | 9 | ...SETTINGS_SECTION_LABELS, |
| 10 | 10 | "/settings/diagnostics": "Diagnostics", |
| 11 | + "/settings/open-source-licenses": "Open source licenses", | |
| 11 | 12 | }; |
| 12 | 13 | |
| 13 | 14 | function settingsBreadcrumbLabel(pathname: string): string | null { |
| @@ -2523,6 +2523,19 @@ export function GeneralSettingsPanel() { · 2522 unmodified lines | ||
| 2523 | 2523 | </Button> |
| 2524 | 2524 | } |
| 2525 | 2525 | /> |
| 2526 | + <SettingsRow | |
| 2527 | + {...searchableSetting("open-source-licenses")} | |
| 2528 | + description="Third-party notices for dependencies and assets included in T3 Code." | |
| 2529 | + control={ | |
| 2530 | + <Button | |
| 2531 | + render={<Link to="/settings/open-source-licenses" />} | |
| 2532 | + size="xs" | |
| 2533 | + variant="outline" | |
| 2534 | + > | |
| 2535 | + View licenses | |
| 2536 | + </Button> | |
| 2537 | + } | |
| 2538 | + /> | |
| 2526 | 2539 | </SettingsSection> |
| 2527 | 2540 | |
| 2528 | 2541 | <LegacyFeaturesSection /> |
| @@ -267,7 +267,14 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 266 unmodified lines | ||
| 267 | 267 | )) |
| 268 | 268 | : SETTINGS_NAV_ITEMS.map((item) => { |
| 269 | 269 | const Icon = item.icon; |
| 270 | − const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`); | |
| 270 | + const isGeneralDetailPage = | |
| 271 | + item.to === "/settings/general" && | |
| 272 | + (pathname === "/settings/diagnostics" || | |
| 273 | + pathname === "/settings/open-source-licenses"); | |
| 274 | + const isActive = | |
| 275 | + isGeneralDetailPage || | |
| 276 | + pathname === item.to || | |
| 277 | + pathname.startsWith(`${item.to}/`); | |
| 271 | 278 | return ( |
| 272 | 279 | <SidebarMenuItem key={item.to}> |
| 273 | 280 | <SidebarMenuButton |
| @@ -188,6 +188,11 @@ export const SETTINGS_SEARCH_ITEMS = [ · 187 unmodified lines | ||
| 188 | 188 | title: "Diagnostics", |
| 189 | 189 | to: "/settings/general", |
| 190 | 190 | }, |
| 191 | + { | |
| 192 | + id: "open-source-licenses", | |
| 193 | + title: "Open source licenses", | |
| 194 | + to: "/settings/general", | |
| 195 | + }, | |
| 191 | 196 | { |
| 192 | 197 | id: "legacy-plan-mode", |
| 193 | 198 | title: "Plan mode (legacy)", |
| @@ -17,6 +17,7 @@ import { Route as ChatRouteImport } from './routes/_chat' · 16 unmodified lines | ||
| 17 | 17 | import { Route as ChatIndexRouteImport } from './routes/_chat.index' |
| 18 | 18 | import { Route as SettingsSourceControlRouteImport } from './routes/settings.source-control' |
| 19 | 19 | import { Route as SettingsProvidersRouteImport } from './routes/settings.providers' |
| 20 | +import { Route as SettingsOpenSourceLicensesRouteImport } from './routes/settings.open-source-licenses' | |
| 20 | 21 | import { Route as SettingsKeybindingsRouteImport } from './routes/settings.keybindings' |
| 21 | 22 | import { Route as SettingsIntegrationsRouteImport } from './routes/settings.integrations' |
| 22 | 23 | import { Route as SettingsGeneralRouteImport } from './routes/settings.general' |
| @@ -69,6 +70,12 @@ const SettingsProvidersRoute = SettingsProvidersRouteImport.update({ · 46 unmodified lines | ||
| 69 | 70 | path: '/providers', |
| 70 | 71 | getParentRoute: () => SettingsRoute, |
| 71 | 72 | } as any) |
| 73 | +const SettingsOpenSourceLicensesRoute = | |
| 74 | + SettingsOpenSourceLicensesRouteImport.update({ | |
| 75 | + id: '/open-source-licenses', | |
| 76 | + path: '/open-source-licenses', | |
| 77 | + getParentRoute: () => SettingsRoute, | |
| 78 | + } as any) | |
| 72 | 79 | const SettingsKeybindingsRoute = SettingsKeybindingsRouteImport.update({ |
| 73 | 80 | id: '/keybindings', |
| 74 | 81 | path: '/keybindings', |
| @@ -147,6 +154,7 @@ export interface FileRoutesByFullPath { · 72 unmodified lines | ||
| 147 | 154 | '/settings/general': typeof SettingsGeneralRoute |
| 148 | 155 | '/settings/integrations': typeof SettingsIntegrationsRoute |
| 149 | 156 | '/settings/keybindings': typeof SettingsKeybindingsRoute |
| 157 | + '/settings/open-source-licenses': typeof SettingsOpenSourceLicensesRoute | |
| 150 | 158 | '/settings/providers': typeof SettingsProvidersRoute |
| 151 | 159 | '/settings/source-control': typeof SettingsSourceControlRoute |
| 152 | 160 | '/$environmentId/$threadId': typeof ChatEnvironmentIdThreadIdRoute |
| @@ -167,6 +175,7 @@ export interface FileRoutesByTo { · 14 unmodified lines | ||
| 167 | 175 | '/settings/general': typeof SettingsGeneralRoute |
| 168 | 176 | '/settings/integrations': typeof SettingsIntegrationsRoute |
| 169 | 177 | '/settings/keybindings': typeof SettingsKeybindingsRoute |
| 178 | + '/settings/open-source-licenses': typeof SettingsOpenSourceLicensesRoute | |
| 170 | 179 | '/settings/providers': typeof SettingsProvidersRoute |
| 171 | 180 | '/settings/source-control': typeof SettingsSourceControlRoute |
| 172 | 181 | '/': typeof ChatIndexRoute |
| @@ -190,6 +199,7 @@ export interface FileRoutesById { · 17 unmodified lines | ||
| 190 | 199 | '/settings/general': typeof SettingsGeneralRoute |
| 191 | 200 | '/settings/integrations': typeof SettingsIntegrationsRoute |
| 192 | 201 | '/settings/keybindings': typeof SettingsKeybindingsRoute |
| 202 | + '/settings/open-source-licenses': typeof SettingsOpenSourceLicensesRoute | |
| 193 | 203 | '/settings/providers': typeof SettingsProvidersRoute |
| 194 | 204 | '/settings/source-control': typeof SettingsSourceControlRoute |
| 195 | 205 | '/_chat/': typeof ChatIndexRoute |
| @@ -214,6 +224,7 @@ export interface FileRouteTypes { · 18 unmodified lines | ||
| 214 | 224 | | '/settings/general' |
| 215 | 225 | | '/settings/integrations' |
| 216 | 226 | | '/settings/keybindings' |
| 227 | + | '/settings/open-source-licenses' | |
| 217 | 228 | | '/settings/providers' |
| 218 | 229 | | '/settings/source-control' |
| 219 | 230 | | '/$environmentId/$threadId' |
| @@ -234,6 +245,7 @@ export interface FileRouteTypes { · 14 unmodified lines | ||
| 234 | 245 | | '/settings/general' |
| 235 | 246 | | '/settings/integrations' |
| 236 | 247 | | '/settings/keybindings' |
| 248 | + | '/settings/open-source-licenses' | |
| 237 | 249 | | '/settings/providers' |
| 238 | 250 | | '/settings/source-control' |
| 239 | 251 | | '/' |
| @@ -256,6 +268,7 @@ export interface FileRouteTypes { · 16 unmodified lines | ||
| 256 | 268 | | '/settings/general' |
| 257 | 269 | | '/settings/integrations' |
| 258 | 270 | | '/settings/keybindings' |
| 271 | + | '/settings/open-source-licenses' | |
| 259 | 272 | | '/settings/providers' |
| 260 | 273 | | '/settings/source-control' |
| 261 | 274 | | '/_chat/' |
| @@ -331,6 +344,13 @@ declare module '@tanstack/react-router' { · 69 unmodified lines | ||
| 331 | 344 | preLoaderRoute: typeof SettingsProvidersRouteImport |
| 332 | 345 | parentRoute: typeof SettingsRoute |
| 333 | 346 | } |
| 347 | + '/settings/open-source-licenses': { | |
| 348 | + id: '/settings/open-source-licenses' | |
| 349 | + path: '/open-source-licenses' | |
| 350 | + fullPath: '/settings/open-source-licenses' | |
| 351 | + preLoaderRoute: typeof SettingsOpenSourceLicensesRouteImport | |
| 352 | + parentRoute: typeof SettingsRoute | |
| 353 | + } | |
| 334 | 354 | '/settings/keybindings': { |
| 335 | 355 | id: '/settings/keybindings' |
| 336 | 356 | path: '/keybindings' |
| @@ -442,6 +462,7 @@ interface SettingsRouteChildren { · 105 unmodified lines | ||
| 442 | 462 | SettingsGeneralRoute: typeof SettingsGeneralRoute |
| 443 | 463 | SettingsIntegrationsRoute: typeof SettingsIntegrationsRoute |
| 444 | 464 | SettingsKeybindingsRoute: typeof SettingsKeybindingsRoute |
| 465 | + SettingsOpenSourceLicensesRoute: typeof SettingsOpenSourceLicensesRoute | |
| 445 | 466 | SettingsProvidersRoute: typeof SettingsProvidersRoute |
| 446 | 467 | SettingsSourceControlRoute: typeof SettingsSourceControlRoute |
| 447 | 468 | } |
| @@ -454,6 +475,7 @@ const SettingsRouteChildren: SettingsRouteChildren = { · 6 unmodified lines | ||
| 454 | 475 | SettingsGeneralRoute: SettingsGeneralRoute, |
| 455 | 476 | SettingsIntegrationsRoute: SettingsIntegrationsRoute, |
| 456 | 477 | SettingsKeybindingsRoute: SettingsKeybindingsRoute, |
| 478 | + SettingsOpenSourceLicensesRoute: SettingsOpenSourceLicensesRoute, | |
| 457 | 479 | SettingsProvidersRoute: SettingsProvidersRoute, |
| 458 | 480 | SettingsSourceControlRoute: SettingsSourceControlRoute, |
| 459 | 481 | } |
| @@ -0,0 +1,7 @@ | ||
| 1 | +import { createFileRoute } from "@tanstack/react-router"; | |
| 2 | + | |
| 3 | +import { OpenSourceLicensesPanel } from "../components/settings/OpenSourceLicenses"; | |
| 4 | + | |
| 5 | +export const Route = createFileRoute("/settings/open-source-licenses")({ | |
| 6 | + component: OpenSourceLicensesPanel, | |
| 7 | +}); | |
| @@ -31,6 +31,7 @@ · 30 unmodified lines | ||
| 31 | 31 | "vercel.ts", |
| 32 | 32 | "test", |
| 33 | 33 | "scripts/warm-dep-cache.ts", |
| 34 | − "../../scripts/lib/public-config.ts" | |
| 34 | + "../../scripts/lib/public-config.ts", | |
| 35 | + "../../scripts/lib/third-party-licenses.ts" | |
| 35 | 36 | ] |
| 36 | 37 | } |
| @@ -13,6 +13,7 @@ import pkg from "./package.json" with { type: "json" }; · 12 unmodified lines | ||
| 13 | 13 | import { DEV_PROXIED_PATH_PREFIXES } from "@t3tools/shared/devProxy"; |
| 14 | 14 | |
| 15 | 15 | import { loadRepoEnv } from "../../scripts/lib/public-config"; |
| 16 | +import { thirdPartyLicensesPlugin } from "../../scripts/lib/third-party-licenses"; | |
| 16 | 17 | |
| 17 | 18 | const repoEnv = loadRepoEnv(); |
| 18 | 19 | Object.assign(process.env, repoEnv); |
| @@ -156,6 +157,15 @@ export default defineConfig(() => { · 137 unmodified lines | ||
| 156 | 157 | assetsInclude: ["**/*.wasm"], |
| 157 | 158 | plugins: [ |
| 158 | 159 | devCompressionPlugin(), |
| 160 | + thirdPartyLicensesPlugin({ | |
| 161 | + bundleName: "web", | |
| 162 | + configFile: new URL("../../third-party-licenses.config.json", import.meta.url), | |
| 163 | + packageManifests: [ | |
| 164 | + { bundle: "web", path: new URL("./package.json", import.meta.url) }, | |
| 165 | + { bundle: "server", path: new URL("../server/package.json", import.meta.url) }, | |
| 166 | + { bundle: "desktop", path: new URL("../desktop/package.json", import.meta.url) }, | |
| 167 | + ], | |
| 168 | + }), | |
| 159 | 169 | tanstackRouter(), |
| 160 | 170 | react(), |
| 161 | 171 | babel({ |
| @@ -0,0 +1,84 @@ | ||
| 1 | +# Open source license notices | |
| 2 | + | |
| 3 | +License notices are generated independently for the client that ships them: | |
| 4 | + | |
| 5 | +- The web build emits `third-party-licenses.json` beside `index.html`. The Settings page loads that | |
| 6 | + static file, so the same artifact works in hosted web, the client bundled with `npx t3`, and | |
| 7 | + desktop. | |
| 8 | +- The mobile Metro config generates an ignored virtual module before each development, native, or | |
| 9 | + over-the-air JavaScript bundle. Mobile reads the manifest directly from that module and does not | |
| 10 | + need a network request. | |
| 11 | + | |
| 12 | +Neither path depends on the connected environment or an RPC. | |
| 13 | + | |
| 14 | +## What the build collects | |
| 15 | + | |
| 16 | +The generator follows installed production and optional dependencies, including dependencies of | |
| 17 | +workspace packages, and omits first-party `@t3tools/*` packages. The web manifest starts from the | |
| 18 | +web, server, and desktop package manifests. The mobile manifest starts from the mobile package | |
| 19 | +manifest. During the web bundle, the generator also checks emitted module ids to catch a bundled | |
| 20 | +npm import missing from a package manifest. | |
| 21 | + | |
| 22 | +The mobile manifest deliberately follows the complete production dependency closure declared by | |
| 23 | +Expo and React Native. That is conservative and can include build tooling that is not present in | |
| 24 | +the final JavaScript bundle, but it avoids dropping a notice when platform bundling changes. | |
| 25 | + | |
| 26 | +The build fails when a collected package has no distributable license identifier or contains no | |
| 27 | +license or notice file. This turns missing attribution into a build error instead of an empty row | |
| 28 | +in a release. | |
| 29 | + | |
| 30 | +## Custom notices and package overrides | |
| 31 | + | |
| 32 | +The repository-level `third-party-licenses.config.json` holds manually maintained exceptions for | |
| 33 | +all clients. Add an entry to `customNotices` for adapted icons, fonts, media, native modules, or | |
| 34 | +another asset that did not come from an npm package: | |
| 35 | + | |
| 36 | +```json | |
| 37 | +{ | |
| 38 | + "name": "asset-name", | |
| 39 | + "license": "CC-BY-4.0", | |
| 40 | + "noticeFile": "licenses/asset-name.txt", | |
| 41 | + "sourceUrl": "https://example.com/source", | |
| 42 | + "bundles": ["assets", "web"] | |
| 43 | +} | |
| 44 | +``` | |
| 45 | + | |
| 46 | +Paths in `noticeFile` are relative to the config file. The notice file should contain the complete | |
| 47 | +copyright, attribution, and license text required for redistribution. `bundles` controls which | |
| 48 | +generated manifests include the entry; omit it only when the notice belongs in every client. | |
| 49 | + | |
| 50 | +Use `packageOverrides` only when an installed npm archive omits its notice or has incorrect | |
| 51 | +metadata: | |
| 52 | + | |
| 53 | +```json | |
| 54 | +{ | |
| 55 | + "name": "package-name", | |
| 56 | + "version": "1.2.3", | |
| 57 | + "noticeFile": "licenses/package-name-1.2.3.txt", | |
| 58 | + "license": "MIT", | |
| 59 | + "sourceUrl": "https://example.com/package-name" | |
| 60 | +} | |
| 61 | +``` | |
| 62 | + | |
| 63 | +`version`, `license`, and `sourceUrl` are optional. Omitting `version` applies the override to every | |
| 64 | +installed version of that package. An override can use `repositoryUrl` instead of `name` when | |
| 65 | +several packages from one monorepo share the same notice: | |
| 66 | + | |
| 67 | +```json | |
| 68 | +{ | |
| 69 | + "repositoryUrl": "https://github.com/example/project", | |
| 70 | + "noticeFile": "licenses/project.txt" | |
| 71 | +} | |
| 72 | +``` | |
| 73 | + | |
| 74 | +The generator also reuses an installed sibling package's notice when both packages declare the | |
| 75 | +same normalized repository and license. A name-and-version override always wins over these | |
| 76 | +repository fallbacks. | |
| 77 | + | |
| 78 | +The `@react-grab/cli` override uses the root React Grab repository's MIT license because the CLI's | |
| 79 | +npm archive omits both its license field and license file. Keep the override until the published | |
| 80 | +CLI package carries that metadata itself. | |
| 81 | + | |
| 82 | +Generated mobile files live under `apps/mobile/.generated/` and are ignored. Do not commit or edit | |
| 83 | +them; updating dependencies, configuration, or a notice file is enough for the next Metro startup | |
| 84 | +to refresh the manifest. | |
| @@ -0,0 +1,12 @@ | ||
| 1 | +# Open source licenses | |
| 2 | + | |
| 3 | +T3 Code includes third-party software and adapted assets. To read their license and attribution | |
| 4 | +notices: | |
| 5 | + | |
| 6 | +- On web and desktop, open **Settings → General**, find **Open source licenses** under **About**, | |
| 7 | + and select **View licenses**. | |
| 8 | +- On mobile, open **Settings → App → Open source licenses**. | |
| 9 | + | |
| 10 | +The page lists the package version, license identifier, and the parts of T3 Code that may include | |
| 11 | +it. Select a row to read the complete notice text. Use the search field to find a package, version, | |
| 12 | +license, or app component. | |
| @@ -231,6 +231,10 @@ · 230 unmodified lines | ||
| 231 | 231 | "types": "./src/usageFormat.ts", |
| 232 | 232 | "import": "./src/usageFormat.ts" |
| 233 | 233 | }, |
| 234 | + "./thirdPartyLicenses": { | |
| 235 | + "types": "./src/thirdPartyLicenses.ts", | |
| 236 | + "import": "./src/thirdPartyLicenses.ts" | |
| 237 | + }, | |
| 234 | 238 | "./claudeCompaction": { |
| 235 | 239 | "types": "./src/claudeCompaction.ts", |
| 236 | 240 | "import": "./src/claudeCompaction.ts" |
| @@ -0,0 +1,63 @@ | ||
| 1 | +import { describe, expect, it } from "vite-plus/test"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + decodeThirdPartyLicenseManifest, | |
| 5 | + filterThirdPartyLicenseEntries, | |
| 6 | + findThirdPartyLicenseEntry, | |
| 7 | + formatLicenseBundles, | |
| 8 | + thirdPartyLicenseEntryKey, | |
| 9 | + type ThirdPartyLicenseEntry, | |
| 10 | +} from "./thirdPartyLicenses.js"; | |
| 11 | + | |
| 12 | +const ENTRIES: ReadonlyArray<ThirdPartyLicenseEntry> = [ | |
| 13 | + { | |
| 14 | + bundles: ["web"], | |
| 15 | + kind: "package", | |
| 16 | + license: "MIT", | |
| 17 | + name: "react", | |
| 18 | + noticeText: "React license", | |
| 19 | + sourceUrl: "https://react.dev", | |
| 20 | + version: "19.2.6", | |
| 21 | + }, | |
| 22 | + { | |
| 23 | + bundles: ["assets", "mobile"], | |
| 24 | + kind: "custom", | |
| 25 | + license: "CC-BY-4.0", | |
| 26 | + name: "sample-icons", | |
| 27 | + noticeText: "Icon notice", | |
| 28 | + sourceUrl: null, | |
| 29 | + version: null, | |
| 30 | + }, | |
| 31 | +]; | |
| 32 | + | |
| 33 | +describe("third-party license manifests", () => { | |
| 34 | + it("decodes the generated manifest shape", () => { | |
| 35 | + expect(decodeThirdPartyLicenseManifest({ schemaVersion: 1, entries: ENTRIES })).toEqual({ | |
| 36 | + schemaVersion: 1, | |
| 37 | + entries: ENTRIES, | |
| 38 | + }); | |
| 39 | + }); | |
| 40 | + | |
| 41 | + it("rejects unsupported manifest versions", () => { | |
| 42 | + expect(() => decodeThirdPartyLicenseManifest({ schemaVersion: 2, entries: [] })).toThrow( | |
| 43 | + "unsupported format", | |
| 44 | + ); | |
| 45 | + }); | |
| 46 | + | |
| 47 | + it("filters by package, license, version, and bundle", () => { | |
| 48 | + expect(filterThirdPartyLicenseEntries(ENTRIES, "react 19.2")).toEqual([ENTRIES[0]]); | |
| 49 | + expect(filterThirdPartyLicenseEntries(ENTRIES, "cc-by mobile")).toEqual([ENTRIES[1]]); | |
| 50 | + expect(filterThirdPartyLicenseEntries(ENTRIES, "apache")).toEqual([]); | |
| 51 | + }); | |
| 52 | + | |
| 53 | + it("formats platform bundle names for display", () => { | |
| 54 | + expect(formatLicenseBundles(["android", "assets", "ios", "mobile", "plugin"])).toBe( | |
| 55 | + "Android, Assets, iOS, Mobile, plugin", | |
| 56 | + ); | |
| 57 | + }); | |
| 58 | + | |
| 59 | + it("finds an entry by its stable navigation key", () => { | |
| 60 | + const entry = ENTRIES[0]!; | |
| 61 | + expect(findThirdPartyLicenseEntry(ENTRIES, thirdPartyLicenseEntryKey(entry))).toBe(entry); | |
| 62 | + }); | |
| 63 | +}); | |
| @@ -0,0 +1,103 @@ | ||
| 1 | +export interface ThirdPartyLicenseEntry { | |
| 2 | + readonly bundles: ReadonlyArray<string>; | |
| 3 | + readonly kind: "custom" | "package"; | |
| 4 | + readonly license: string; | |
| 5 | + readonly name: string; | |
| 6 | + readonly noticeText: string; | |
| 7 | + readonly sourceUrl: string | null; | |
| 8 | + readonly version: string | null; | |
| 9 | +} | |
| 10 | + | |
| 11 | +export interface ThirdPartyLicenseManifest { | |
| 12 | + readonly schemaVersion: 1; | |
| 13 | + readonly entries: ReadonlyArray<ThirdPartyLicenseEntry>; | |
| 14 | +} | |
| 15 | + | |
| 16 | +function isRecord(value: unknown): value is Record<string, unknown> { | |
| 17 | + return typeof value === "object" && value !== null && !Array.isArray(value); | |
| 18 | +} | |
| 19 | + | |
| 20 | +function isStringArray(value: unknown): value is ReadonlyArray<string> { | |
| 21 | + return Array.isArray(value) && value.every((entry) => typeof entry === "string"); | |
| 22 | +} | |
| 23 | + | |
| 24 | +function decodeEntry(value: unknown, index: number): ThirdPartyLicenseEntry { | |
| 25 | + if (!isRecord(value)) { | |
| 26 | + throw new Error(`License entry ${String(index + 1)} is not an object.`); | |
| 27 | + } | |
| 28 | + if ( | |
| 29 | + !isStringArray(value.bundles) || | |
| 30 | + value.bundles.length === 0 || | |
| 31 | + (value.kind !== "custom" && value.kind !== "package") || | |
| 32 | + typeof value.license !== "string" || | |
| 33 | + typeof value.name !== "string" || | |
| 34 | + typeof value.noticeText !== "string" || | |
| 35 | + (value.sourceUrl !== null && typeof value.sourceUrl !== "string") || | |
| 36 | + (value.version !== null && typeof value.version !== "string") | |
| 37 | + ) { | |
| 38 | + throw new Error(`License entry ${String(index + 1)} has an invalid shape.`); | |
| 39 | + } | |
| 40 | + return { | |
| 41 | + bundles: value.bundles, | |
| 42 | + kind: value.kind, | |
| 43 | + license: value.license, | |
| 44 | + name: value.name, | |
| 45 | + noticeText: value.noticeText, | |
| 46 | + sourceUrl: value.sourceUrl, | |
| 47 | + version: value.version, | |
| 48 | + }; | |
| 49 | +} | |
| 50 | + | |
| 51 | +export function decodeThirdPartyLicenseManifest(value: unknown): ThirdPartyLicenseManifest { | |
| 52 | + if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.entries)) { | |
| 53 | + throw new Error("The open-source license manifest has an unsupported format."); | |
| 54 | + } | |
| 55 | + return { | |
| 56 | + schemaVersion: 1, | |
| 57 | + entries: value.entries.map(decodeEntry), | |
| 58 | + }; | |
| 59 | +} | |
| 60 | + | |
| 61 | +export function filterThirdPartyLicenseEntries( | |
| 62 | + entries: ReadonlyArray<ThirdPartyLicenseEntry>, | |
| 63 | + query: string, | |
| 64 | +): ReadonlyArray<ThirdPartyLicenseEntry> { | |
| 65 | + const terms = query | |
| 66 | + .trim() | |
| 67 | + .toLocaleLowerCase() | |
| 68 | + .split(/\s+/) | |
| 69 | + .filter((term) => term.length > 0); | |
| 70 | + if (terms.length === 0) return entries; | |
| 71 | + return entries.filter((entry) => { | |
| 72 | + const searchable = [entry.name, entry.version, entry.license, ...entry.bundles] | |
| 73 | + .filter((value): value is string => value !== null) | |
| 74 | + .join(" ") | |
| 75 | + .toLocaleLowerCase(); | |
| 76 | + return terms.every((term) => searchable.includes(term)); | |
| 77 | + }); | |
| 78 | +} | |
| 79 | + | |
| 80 | +const BUNDLE_LABELS: Readonly<Record<string, string>> = { | |
| 81 | + android: "Android", | |
| 82 | + assets: "Assets", | |
| 83 | + desktop: "Desktop", | |
| 84 | + ios: "iOS", | |
| 85 | + mobile: "Mobile", | |
| 86 | + server: "Server", | |
| 87 | + web: "Web", | |
| 88 | +}; | |
| 89 | + | |
| 90 | +export function formatLicenseBundles(bundles: ReadonlyArray<string>): string { | |
| 91 | + return bundles.map((bundle) => BUNDLE_LABELS[bundle] ?? bundle).join(", "); | |
| 92 | +} | |
| 93 | + | |
| 94 | +export function thirdPartyLicenseEntryKey(entry: ThirdPartyLicenseEntry): string { | |
| 95 | + return `${entry.kind}:${entry.name}:${entry.version ?? "custom"}`; | |
| 96 | +} | |
| 97 | + | |
| 98 | +export function findThirdPartyLicenseEntry( | |
| 99 | + entries: ReadonlyArray<ThirdPartyLicenseEntry>, | |
| 100 | + key: string, | |
| 101 | +): ThirdPartyLicenseEntry | undefined { | |
| 102 | + return entries.find((entry) => thirdPartyLicenseEntryKey(entry) === key); | |
| 103 | +} | |
| @@ -0,0 +1,245 @@ | ||
| 1 | +// @effect-diagnostics nodeBuiltinImport:off - Tests exercise the Node filesystem build boundary. | |
| 2 | + | |
| 3 | +import * as NodeFSP from "node:fs/promises"; | |
| 4 | +import * as NodeOS from "node:os"; | |
| 5 | +import * as NodePath from "node:path"; | |
| 6 | + | |
| 7 | +import { afterEach, describe, expect, it } from "vite-plus/test"; | |
| 8 | + | |
| 9 | +import { generateThirdPartyLicenseManifest } from "./third-party-licenses.js"; | |
| 10 | + | |
| 11 | +const tempDirectories: string[] = []; | |
| 12 | + | |
| 13 | +async function writeJson(path: string, value: unknown): Promise<void> { | |
| 14 | + await NodeFSP.mkdir(NodePath.dirname(path), { recursive: true }); | |
| 15 | + await NodeFSP.writeFile(path, `${JSON.stringify(value)}\n`, "utf8"); | |
| 16 | +} | |
| 17 | + | |
| 18 | +async function createFixture(): Promise<{ | |
| 19 | + readonly appManifest: string; | |
| 20 | + readonly configFile: string; | |
| 21 | + readonly dependencyRoot: string; | |
| 22 | + readonly root: string; | |
| 23 | +}> { | |
| 24 | + const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3code-licenses-")); | |
| 25 | + tempDirectories.push(root); | |
| 26 | + const appManifest = NodePath.join(root, "package.json"); | |
| 27 | + const dependencyRoot = NodePath.join(root, "node_modules", "demo-dependency"); | |
| 28 | + const configFile = NodePath.join(root, "third-party-licenses.config.json"); | |
| 29 | + | |
| 30 | + await writeJson(appManifest, { | |
| 31 | + name: "fixture-app", | |
| 32 | + dependencies: { "demo-dependency": "1.2.3" }, | |
| 33 | + }); | |
| 34 | + await writeJson(NodePath.join(dependencyRoot, "package.json"), { | |
| 35 | + name: "demo-dependency", | |
| 36 | + version: "1.2.3", | |
| 37 | + license: "MIT", | |
| 38 | + main: "index.js", | |
| 39 | + repository: "example/demo-dependency", | |
| 40 | + }); | |
| 41 | + await NodeFSP.writeFile(NodePath.join(dependencyRoot, "index.js"), "export {};\n", "utf8"); | |
| 42 | + await NodeFSP.writeFile( | |
| 43 | + NodePath.join(dependencyRoot, "LICENSE"), | |
| 44 | + "Demo MIT license text\n", | |
| 45 | + "utf8", | |
| 46 | + ); | |
| 47 | + await NodeFSP.writeFile(NodePath.join(root, "asset-notice.txt"), "Asset notice text\n", "utf8"); | |
| 48 | + await writeJson(configFile, { | |
| 49 | + customNotices: [ | |
| 50 | + { | |
| 51 | + name: "demo-asset", | |
| 52 | + license: "CC-BY-4.0", | |
| 53 | + noticeFile: "asset-notice.txt", | |
| 54 | + bundles: ["assets", "web"], | |
| 55 | + }, | |
| 56 | + ], | |
| 57 | + packageOverrides: [], | |
| 58 | + }); | |
| 59 | + return { appManifest, configFile, dependencyRoot, root }; | |
| 60 | +} | |
| 61 | + | |
| 62 | +afterEach(async () => { | |
| 63 | + await Promise.all( | |
| 64 | + tempDirectories | |
| 65 | + .splice(0) | |
| 66 | + .map((directory) => NodeFSP.rm(directory, { force: true, recursive: true })), | |
| 67 | + ); | |
| 68 | +}); | |
| 69 | + | |
| 70 | +describe("third-party license generation", () => { | |
| 71 | + it("collects production packages and custom asset notices", async () => { | |
| 72 | + const fixture = await createFixture(); | |
| 73 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 74 | + configFile: fixture.configFile, | |
| 75 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 76 | + }); | |
| 77 | + | |
| 78 | + expect(manifest).toEqual({ | |
| 79 | + schemaVersion: 1, | |
| 80 | + entries: [ | |
| 81 | + { | |
| 82 | + bundles: ["assets", "web"], | |
| 83 | + kind: "custom", | |
| 84 | + license: "CC-BY-4.0", | |
| 85 | + name: "demo-asset", | |
| 86 | + noticeText: "Asset notice text", | |
| 87 | + sourceUrl: null, | |
| 88 | + version: null, | |
| 89 | + }, | |
| 90 | + { | |
| 91 | + bundles: ["web"], | |
| 92 | + kind: "package", | |
| 93 | + license: "MIT", | |
| 94 | + name: "demo-dependency", | |
| 95 | + noticeText: "Demo MIT license text", | |
| 96 | + sourceUrl: "https://github.com/example/demo-dependency", | |
| 97 | + version: "1.2.3", | |
| 98 | + }, | |
| 99 | + ], | |
| 100 | + }); | |
| 101 | + }); | |
| 102 | + | |
| 103 | + it("fails when a production package has no distributable notice text", async () => { | |
| 104 | + const fixture = await createFixture(); | |
| 105 | + await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE")); | |
| 106 | + | |
| 107 | + await expect( | |
| 108 | + generateThirdPartyLicenseManifest({ | |
| 109 | + configFile: fixture.configFile, | |
| 110 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 111 | + }), | |
| 112 | + ).rejects.toThrow("does not include a license or notice file"); | |
| 113 | + }); | |
| 114 | + | |
| 115 | + it("collects notices nested inside a published package", async () => { | |
| 116 | + const fixture = await createFixture(); | |
| 117 | + await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE")); | |
| 118 | + await NodeFSP.mkdir(NodePath.join(fixture.dependencyRoot, "dist", "third-party"), { | |
| 119 | + recursive: true, | |
| 120 | + }); | |
| 121 | + await NodeFSP.writeFile( | |
| 122 | + NodePath.join(fixture.dependencyRoot, "dist", "third-party", "NOTICE.txt"), | |
| 123 | + "Nested notice\n", | |
| 124 | + "utf8", | |
| 125 | + ); | |
| 126 | + | |
| 127 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 128 | + configFile: fixture.configFile, | |
| 129 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 130 | + }); | |
| 131 | + | |
| 132 | + expect(manifest.entries.find((entry) => entry.name === "demo-dependency")?.noticeText).toBe( | |
| 133 | + "Nested notice", | |
| 134 | + ); | |
| 135 | + }); | |
| 136 | + | |
| 137 | + it("uses package overrides for notices published outside the npm archive", async () => { | |
| 138 | + const fixture = await createFixture(); | |
| 139 | + await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE")); | |
| 140 | + await NodeFSP.writeFile(NodePath.join(fixture.root, "override.txt"), "Override text\n", "utf8"); | |
| 141 | + await writeJson(fixture.configFile, { | |
| 142 | + customNotices: [], | |
| 143 | + packageOverrides: [ | |
| 144 | + { | |
| 145 | + name: "demo-dependency", | |
| 146 | + noticeFile: "override.txt", | |
| 147 | + }, | |
| 148 | + ], | |
| 149 | + }); | |
| 150 | + | |
| 151 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 152 | + configFile: fixture.configFile, | |
| 153 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 154 | + }); | |
| 155 | + | |
| 156 | + expect(manifest.entries[0]?.noticeText).toBe("Override text"); | |
| 157 | + }); | |
| 158 | + | |
| 159 | + it("applies repository overrides across monorepo packages", async () => { | |
| 160 | + const fixture = await createFixture(); | |
| 161 | + await NodeFSP.rm(NodePath.join(fixture.dependencyRoot, "LICENSE")); | |
| 162 | + await NodeFSP.writeFile( | |
| 163 | + NodePath.join(fixture.root, "override.txt"), | |
| 164 | + "Repository text\n", | |
| 165 | + "utf8", | |
| 166 | + ); | |
| 167 | + await writeJson(fixture.configFile, { | |
| 168 | + customNotices: [], | |
| 169 | + packageOverrides: [ | |
| 170 | + { | |
| 171 | + repositoryUrl: "https://github.com/example/demo-dependency", | |
| 172 | + noticeFile: "override.txt", | |
| 173 | + }, | |
| 174 | + ], | |
| 175 | + }); | |
| 176 | + | |
| 177 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 178 | + configFile: fixture.configFile, | |
| 179 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 180 | + }); | |
| 181 | + | |
| 182 | + expect(manifest.entries[0]?.noticeText).toBe("Repository text"); | |
| 183 | + }); | |
| 184 | + | |
| 185 | + it("reuses a repository license for packages from the same monorepo", async () => { | |
| 186 | + const fixture = await createFixture(); | |
| 187 | + const siblingRoot = NodePath.join(fixture.root, "node_modules", "demo-sibling"); | |
| 188 | + await writeJson(fixture.appManifest, { | |
| 189 | + name: "fixture-app", | |
| 190 | + dependencies: { "demo-dependency": "1.2.3", "demo-sibling": "2.0.0" }, | |
| 191 | + }); | |
| 192 | + await writeJson(NodePath.join(siblingRoot, "package.json"), { | |
| 193 | + name: "demo-sibling", | |
| 194 | + version: "2.0.0", | |
| 195 | + license: "MIT", | |
| 196 | + main: "index.js", | |
| 197 | + repository: "example/demo-dependency", | |
| 198 | + }); | |
| 199 | + await NodeFSP.writeFile(NodePath.join(siblingRoot, "index.js"), "export {};\n", "utf8"); | |
| 200 | + | |
| 201 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 202 | + configFile: fixture.configFile, | |
| 203 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 204 | + }); | |
| 205 | + | |
| 206 | + expect(manifest.entries.find((entry) => entry.name === "demo-sibling")?.noticeText).toBe( | |
| 207 | + "Demo MIT license text", | |
| 208 | + ); | |
| 209 | + }); | |
| 210 | + | |
| 211 | + it("omits custom notices for other bundles", async () => { | |
| 212 | + const fixture = await createFixture(); | |
| 213 | + await writeJson(fixture.configFile, { | |
| 214 | + customNotices: [ | |
| 215 | + { | |
| 216 | + name: "web-only-asset", | |
| 217 | + license: "MIT", | |
| 218 | + noticeFile: "asset-notice.txt", | |
| 219 | + bundles: ["assets", "web"], | |
| 220 | + }, | |
| 221 | + ], | |
| 222 | + packageOverrides: [], | |
| 223 | + }); | |
| 224 | + | |
| 225 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 226 | + configFile: fixture.configFile, | |
| 227 | + packageManifests: [{ bundle: "mobile", path: fixture.appManifest }], | |
| 228 | + }); | |
| 229 | + | |
| 230 | + expect(manifest.entries.some((entry) => entry.name === "web-only-asset")).toBe(false); | |
| 231 | + }); | |
| 232 | + | |
| 233 | + it("fails when a custom notice file is empty", async () => { | |
| 234 | + const fixture = await createFixture(); | |
| 235 | + await NodeFSP.writeFile(NodePath.join(fixture.root, "asset-notice.txt"), "\n", "utf8"); | |
| 236 | + | |
| 237 | + await expect( | |
| 238 | + generateThirdPartyLicenseManifest({ | |
| 239 | + configFile: fixture.configFile, | |
| 240 | + packageManifests: [{ bundle: "web", path: fixture.appManifest }], | |
| 241 | + }), | |
| 242 | + ).rejects.toThrow('Custom third-party notice "demo-asset" is empty'); | |
| 243 | + }); | |
| 244 | +}); | |
| 245 | +// @effect-diagnostics nodeBuiltinImport:off - Tests exercise the Node filesystem build boundary. | |
| @@ -0,0 +1,710 @@ | ||
| 1 | +// @effect-diagnostics nodeBuiltinImport:off - Vite's build plugin runs before an Effect runtime exists. | |
| 2 | + | |
| 3 | +import * as NodeFSP from "node:fs/promises"; | |
| 4 | +import * as NodePath from "node:path"; | |
| 5 | +import * as NodeURL from "node:url"; | |
| 6 | +import * as NodeModule from "node:module"; | |
| 7 | + | |
| 8 | +import type { Plugin } from "vite-plus"; | |
| 9 | + | |
| 10 | +export const THIRD_PARTY_LICENSES_FILE_NAME = "third-party-licenses.json"; | |
| 11 | + | |
| 12 | +export interface ThirdPartyLicenseEntry { | |
| 13 | + readonly bundles: ReadonlyArray<string>; | |
| 14 | + readonly kind: "custom" | "package"; | |
| 15 | + readonly license: string; | |
| 16 | + readonly name: string; | |
| 17 | + readonly noticeText: string; | |
| 18 | + readonly sourceUrl: string | null; | |
| 19 | + readonly version: string | null; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export interface ThirdPartyLicenseManifest { | |
| 23 | + readonly schemaVersion: 1; | |
| 24 | + readonly entries: ReadonlyArray<ThirdPartyLicenseEntry>; | |
| 25 | +} | |
| 26 | + | |
| 27 | +export interface ThirdPartyLicensePackageManifest { | |
| 28 | + readonly bundle: string; | |
| 29 | + readonly path: string | URL; | |
| 30 | +} | |
| 31 | + | |
| 32 | +export interface ThirdPartyLicensesPluginOptions { | |
| 33 | + readonly configFile?: string | URL; | |
| 34 | + readonly packageManifests: ReadonlyArray<ThirdPartyLicensePackageManifest>; | |
| 35 | + readonly bundleName: string; | |
| 36 | +} | |
| 37 | + | |
| 38 | +interface PackageJson { | |
| 39 | + readonly dependencies?: Readonly<Record<string, string>>; | |
| 40 | + readonly homepage?: unknown; | |
| 41 | + readonly license?: unknown; | |
| 42 | + readonly licenses?: unknown; | |
| 43 | + readonly name?: unknown; | |
| 44 | + readonly optionalDependencies?: Readonly<Record<string, string>>; | |
| 45 | + readonly repository?: unknown; | |
| 46 | + readonly version?: unknown; | |
| 47 | +} | |
| 48 | + | |
| 49 | +interface CustomNoticeConfigEntry { | |
| 50 | + readonly bundles?: ReadonlyArray<string>; | |
| 51 | + readonly license: string; | |
| 52 | + readonly name: string; | |
| 53 | + readonly noticeFile: string; | |
| 54 | + readonly sourceUrl?: string; | |
| 55 | + readonly version?: string; | |
| 56 | +} | |
| 57 | + | |
| 58 | +interface PackageNoticeOverrideConfigEntry { | |
| 59 | + readonly license?: string; | |
| 60 | + readonly name?: string; | |
| 61 | + readonly noticeFile?: string; | |
| 62 | + readonly repositoryUrl?: string; | |
| 63 | + readonly sourceUrl?: string; | |
| 64 | + readonly version?: string; | |
| 65 | +} | |
| 66 | + | |
| 67 | +interface ThirdPartyLicensesConfig { | |
| 68 | + readonly customNotices: ReadonlyArray<CustomNoticeConfigEntry>; | |
| 69 | + readonly packageOverrides: ReadonlyArray<PackageNoticeOverrideConfigEntry>; | |
| 70 | +} | |
| 71 | + | |
| 72 | +interface CollectedPackage { | |
| 73 | + readonly bundles: Set<string>; | |
| 74 | + readonly packageJson: PackageJson; | |
| 75 | + readonly packageRoot: string; | |
| 76 | +} | |
| 77 | + | |
| 78 | +interface PackageCollection { | |
| 79 | + readonly byIdentity: Map<string, CollectedPackage>; | |
| 80 | +} | |
| 81 | + | |
| 82 | +const EMPTY_CONFIG: ThirdPartyLicensesConfig = { | |
| 83 | + customNotices: [], | |
| 84 | + packageOverrides: [], | |
| 85 | +}; | |
| 86 | + | |
| 87 | +const NOTICE_FILE_PATTERN = /^(?:licen[cs]e|copying|notice)(?:[._-].*)?$/i; | |
| 88 | +const FIRST_PARTY_PACKAGE_PREFIX = "@t3tools/"; | |
| 89 | + | |
| 90 | +function asPath(value: string | URL): string { | |
| 91 | + return value instanceof URL ? NodeURL.fileURLToPath(value) : NodePath.resolve(value); | |
| 92 | +} | |
| 93 | + | |
| 94 | +function isRecord(value: unknown): value is Record<string, unknown> { | |
| 95 | + return typeof value === "object" && value !== null && !Array.isArray(value); | |
| 96 | +} | |
| 97 | + | |
| 98 | +function readRequiredString(value: Record<string, unknown>, key: string, context: string): string { | |
| 99 | + const field = value[key]; | |
| 100 | + if (typeof field !== "string" || field.trim().length === 0) { | |
| 101 | + throw new Error(`${context} must define a non-empty "${key}" string.`); | |
| 102 | + } | |
| 103 | + return field.trim(); | |
| 104 | +} | |
| 105 | + | |
| 106 | +function readOptionalString( | |
| 107 | + value: Record<string, unknown>, | |
| 108 | + key: string, | |
| 109 | + context: string, | |
| 110 | +): string | undefined { | |
| 111 | + const field = value[key]; | |
| 112 | + if (field === undefined) return undefined; | |
| 113 | + if (typeof field !== "string" || field.trim().length === 0) { | |
| 114 | + throw new Error(`${context} must define "${key}" as a non-empty string when present.`); | |
| 115 | + } | |
| 116 | + return field.trim(); | |
| 117 | +} | |
| 118 | + | |
| 119 | +function readOptionalStringArray( | |
| 120 | + value: Record<string, unknown>, | |
| 121 | + key: string, | |
| 122 | + context: string, | |
| 123 | +): ReadonlyArray<string> | undefined { | |
| 124 | + const field = value[key]; | |
| 125 | + if (field === undefined) return undefined; | |
| 126 | + if ( | |
| 127 | + !Array.isArray(field) || | |
| 128 | + field.length === 0 || | |
| 129 | + field.some((entry) => typeof entry !== "string" || entry.trim().length === 0) | |
| 130 | + ) { | |
| 131 | + throw new Error(`${context} must define "${key}" as a non-empty string array.`); | |
| 132 | + } | |
| 133 | + return field.map((entry) => (entry as string).trim()); | |
| 134 | +} | |
| 135 | + | |
| 136 | +function decodeCustomNotices(value: unknown): ReadonlyArray<CustomNoticeConfigEntry> { | |
| 137 | + if (value === undefined) return []; | |
| 138 | + if (!Array.isArray(value)) { | |
| 139 | + throw new Error('Third-party license config field "customNotices" must be an array.'); | |
| 140 | + } | |
| 141 | + return value.map((entry, index) => { | |
| 142 | + const context = `Third-party custom notice at index ${String(index)}`; | |
| 143 | + if (!isRecord(entry)) throw new Error(`${context} must be an object.`); | |
| 144 | + const version = readOptionalString(entry, "version", context); | |
| 145 | + const sourceUrl = readOptionalString(entry, "sourceUrl", context); | |
| 146 | + const bundles = readOptionalStringArray(entry, "bundles", context); | |
| 147 | + return { | |
| 148 | + name: readRequiredString(entry, "name", context), | |
| 149 | + license: readRequiredString(entry, "license", context), | |
| 150 | + noticeFile: readRequiredString(entry, "noticeFile", context), | |
| 151 | + ...(version !== undefined ? { version } : {}), | |
| 152 | + ...(sourceUrl !== undefined ? { sourceUrl } : {}), | |
| 153 | + ...(bundles !== undefined ? { bundles } : {}), | |
| 154 | + }; | |
| 155 | + }); | |
| 156 | +} | |
| 157 | + | |
| 158 | +function decodePackageOverrides(value: unknown): ReadonlyArray<PackageNoticeOverrideConfigEntry> { | |
| 159 | + if (value === undefined) return []; | |
| 160 | + if (!Array.isArray(value)) { | |
| 161 | + throw new Error('Third-party license config field "packageOverrides" must be an array.'); | |
| 162 | + } | |
| 163 | + return value.map((entry, index) => { | |
| 164 | + const context = `Third-party package override at index ${String(index)}`; | |
| 165 | + if (!isRecord(entry)) throw new Error(`${context} must be an object.`); | |
| 166 | + const name = readOptionalString(entry, "name", context); | |
| 167 | + const repositoryUrl = readOptionalString(entry, "repositoryUrl", context); | |
| 168 | + if ((name === undefined) === (repositoryUrl === undefined)) { | |
| 169 | + throw new Error(`${context} must define exactly one of "name" or "repositoryUrl".`); | |
| 170 | + } | |
| 171 | + const version = readOptionalString(entry, "version", context); | |
| 172 | + const license = readOptionalString(entry, "license", context); | |
| 173 | + const noticeFile = readOptionalString(entry, "noticeFile", context); | |
| 174 | + const sourceUrl = readOptionalString(entry, "sourceUrl", context); | |
| 175 | + return { | |
| 176 | + ...(name !== undefined ? { name } : {}), | |
| 177 | + ...(repositoryUrl !== undefined ? { repositoryUrl } : {}), | |
| 178 | + ...(version !== undefined ? { version } : {}), | |
| 179 | + ...(license !== undefined ? { license } : {}), | |
| 180 | + ...(noticeFile !== undefined ? { noticeFile } : {}), | |
| 181 | + ...(sourceUrl !== undefined ? { sourceUrl } : {}), | |
| 182 | + }; | |
| 183 | + }); | |
| 184 | +} | |
| 185 | + | |
| 186 | +async function readConfig(configFile: string | URL | undefined): Promise<{ | |
| 187 | + readonly config: ThirdPartyLicensesConfig; | |
| 188 | + readonly directory: string; | |
| 189 | +}> { | |
| 190 | + if (configFile === undefined) { | |
| 191 | + return { config: EMPTY_CONFIG, directory: NodePath.resolve(".") }; | |
| 192 | + } | |
| 193 | + const configPath = asPath(configFile); | |
| 194 | + const source = await NodeFSP.readFile(configPath, "utf8"); | |
| 195 | + const decoded = JSON.parse(source) as unknown; | |
| 196 | + if (!isRecord(decoded)) throw new Error("Third-party license config must contain an object."); | |
| 197 | + return { | |
| 198 | + config: { | |
| 199 | + customNotices: decodeCustomNotices(decoded.customNotices), | |
| 200 | + packageOverrides: decodePackageOverrides(decoded.packageOverrides), | |
| 201 | + }, | |
| 202 | + directory: NodePath.dirname(configPath), | |
| 203 | + }; | |
| 204 | +} | |
| 205 | + | |
| 206 | +async function readPackageJson(packageJsonPath: string): Promise<PackageJson> { | |
| 207 | + const source = await NodeFSP.readFile(packageJsonPath, "utf8"); | |
| 208 | + const value = JSON.parse(source) as unknown; | |
| 209 | + if (!isRecord(value)) throw new Error(`Package manifest is not an object: ${packageJsonPath}`); | |
| 210 | + return value as PackageJson; | |
| 211 | +} | |
| 212 | + | |
| 213 | +function packageIdentity(packageJson: PackageJson, packageRoot: string): string { | |
| 214 | + const name = | |
| 215 | + typeof packageJson.name === "string" ? packageJson.name : NodePath.basename(packageRoot); | |
| 216 | + const version = typeof packageJson.version === "string" ? packageJson.version : "unknown"; | |
| 217 | + return `${name}@${version}`; | |
| 218 | +} | |
| 219 | + | |
| 220 | +async function findPackageRoot( | |
| 221 | + resolvedPath: string, | |
| 222 | + expectedName?: string, | |
| 223 | +): Promise<{ readonly packageJson: PackageJson; readonly packageRoot: string } | null> { | |
| 224 | + let current = NodePath.dirname(resolvedPath); | |
| 225 | + const root = NodePath.parse(current).root; | |
| 226 | + | |
| 227 | + while (current !== root) { | |
| 228 | + const packageJsonPath = NodePath.join(current, "package.json"); | |
| 229 | + try { | |
| 230 | + const packageJson = await readPackageJson(packageJsonPath); | |
| 231 | + const matchesExpectedPackage = | |
| 232 | + expectedName !== undefined | |
| 233 | + ? packageJson.name === expectedName | |
| 234 | + : typeof packageJson.name === "string" && typeof packageJson.version === "string"; | |
| 235 | + if (matchesExpectedPackage) { | |
| 236 | + return { packageJson, packageRoot: await NodeFSP.realpath(current) }; | |
| 237 | + } | |
| 238 | + } catch (error) { | |
| 239 | + const code = isRecord(error) && typeof error.code === "string" ? error.code : null; | |
| 240 | + if (code !== "ENOENT" && code !== "ENOTDIR") throw error; | |
| 241 | + } | |
| 242 | + current = NodePath.dirname(current); | |
| 243 | + } | |
| 244 | + return null; | |
| 245 | +} | |
| 246 | + | |
| 247 | +async function resolveDependencyPackage( | |
| 248 | + dependencyName: string, | |
| 249 | + fromPackageJsonPath: string, | |
| 250 | +): Promise<{ readonly packageJson: PackageJson; readonly packageRoot: string } | null> { | |
| 251 | + const requireFromPackage = NodeModule.createRequire(fromPackageJsonPath); | |
| 252 | + const candidates = [`${dependencyName}/package.json`, dependencyName]; | |
| 253 | + for (const candidate of candidates) { | |
| 254 | + try { | |
| 255 | + const resolved = requireFromPackage.resolve(candidate); | |
| 256 | + const found = await findPackageRoot(resolved, dependencyName); | |
| 257 | + if (found) return found; | |
| 258 | + } catch (error) { | |
| 259 | + const code = isRecord(error) && typeof error.code === "string" ? error.code : null; | |
| 260 | + if (code !== "MODULE_NOT_FOUND" && code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw error; | |
| 261 | + } | |
| 262 | + } | |
| 263 | + return null; | |
| 264 | +} | |
| 265 | + | |
| 266 | +function dependencyNames(packageJson: PackageJson): ReadonlyArray<string> { | |
| 267 | + return [ | |
| 268 | + ...new Set([ | |
| 269 | + ...Object.keys(packageJson.dependencies ?? {}), | |
| 270 | + ...Object.keys(packageJson.optionalDependencies ?? {}), | |
| 271 | + ]), | |
| 272 | + ].sort((left, right) => left.localeCompare(right)); | |
| 273 | +} | |
| 274 | + | |
| 275 | +async function collectProductionDependencyPackages( | |
| 276 | + packageManifests: ReadonlyArray<ThirdPartyLicensePackageManifest>, | |
| 277 | +): Promise<PackageCollection> { | |
| 278 | + const collection: PackageCollection = { | |
| 279 | + byIdentity: new Map(), | |
| 280 | + }; | |
| 281 | + const visited = new Set<string>(); | |
| 282 | + | |
| 283 | + const visitManifest = async (packageJsonPath: string, bundle: string): Promise<void> => { | |
| 284 | + const packageJson = await readPackageJson(packageJsonPath); | |
| 285 | + for (const dependencyName of dependencyNames(packageJson)) { | |
| 286 | + const resolved = await resolveDependencyPackage(dependencyName, packageJsonPath); | |
| 287 | + if (!resolved) continue; | |
| 288 | + const visitKey = `${bundle}:${resolved.packageRoot}`; | |
| 289 | + if (visited.has(visitKey)) continue; | |
| 290 | + visited.add(visitKey); | |
| 291 | + | |
| 292 | + const dependencyPackageJsonPath = NodePath.join(resolved.packageRoot, "package.json"); | |
| 293 | + const name = | |
| 294 | + typeof resolved.packageJson.name === "string" ? resolved.packageJson.name : dependencyName; | |
| 295 | + if (!name.startsWith(FIRST_PARTY_PACKAGE_PREFIX)) { | |
| 296 | + const identity = packageIdentity(resolved.packageJson, resolved.packageRoot); | |
| 297 | + const existing = collection.byIdentity.get(identity); | |
| 298 | + if (existing) { | |
| 299 | + existing.bundles.add(bundle); | |
| 300 | + } else { | |
| 301 | + collection.byIdentity.set(identity, { | |
| 302 | + bundles: new Set([bundle]), | |
| 303 | + packageJson: resolved.packageJson, | |
| 304 | + packageRoot: resolved.packageRoot, | |
| 305 | + }); | |
| 306 | + } | |
| 307 | + } | |
| 308 | + | |
| 309 | + await visitManifest(dependencyPackageJsonPath, bundle); | |
| 310 | + } | |
| 311 | + }; | |
| 312 | + | |
| 313 | + for (const manifest of packageManifests) { | |
| 314 | + await visitManifest(asPath(manifest.path), manifest.bundle); | |
| 315 | + } | |
| 316 | + return collection; | |
| 317 | +} | |
| 318 | + | |
| 319 | +function moduleFilePath(moduleId: string): string | null { | |
| 320 | + if (moduleId.startsWith("\0") || moduleId.includes("\0")) return null; | |
| 321 | + const withoutQuery = moduleId.split(/[?#]/, 1)[0] ?? moduleId; | |
| 322 | + const filePath = withoutQuery.startsWith("file:") | |
| 323 | + ? NodeURL.fileURLToPath(withoutQuery) | |
| 324 | + : withoutQuery.replace(/^\/@fs\//, "/"); | |
| 325 | + const normalized = filePath.replaceAll("\\", "/"); | |
| 326 | + return normalized.includes("/node_modules/") ? filePath : null; | |
| 327 | +} | |
| 328 | + | |
| 329 | +async function addBundledModulePackages( | |
| 330 | + collection: PackageCollection, | |
| 331 | + moduleIds: ReadonlyArray<string>, | |
| 332 | + bundle: string, | |
| 333 | +): Promise<void> { | |
| 334 | + for (const moduleId of moduleIds) { | |
| 335 | + const filePath = moduleFilePath(moduleId); | |
| 336 | + if (!filePath) continue; | |
| 337 | + let found: Awaited<ReturnType<typeof findPackageRoot>>; | |
| 338 | + try { | |
| 339 | + found = await findPackageRoot(await NodeFSP.realpath(filePath)); | |
| 340 | + } catch (error) { | |
| 341 | + const code = isRecord(error) && typeof error.code === "string" ? error.code : null; | |
| 342 | + if (code === "ENOENT" || code === "ENOTDIR") continue; | |
| 343 | + throw error; | |
| 344 | + } | |
| 345 | + if (!found || typeof found.packageJson.name !== "string") continue; | |
| 346 | + if (found.packageJson.name.startsWith(FIRST_PARTY_PACKAGE_PREFIX)) continue; | |
| 347 | + const identity = packageIdentity(found.packageJson, found.packageRoot); | |
| 348 | + const existing = collection.byIdentity.get(identity); | |
| 349 | + if (existing) { | |
| 350 | + existing.bundles.add(bundle); | |
| 351 | + } else { | |
| 352 | + collection.byIdentity.set(identity, { | |
| 353 | + bundles: new Set([bundle]), | |
| 354 | + packageJson: found.packageJson, | |
| 355 | + packageRoot: found.packageRoot, | |
| 356 | + }); | |
| 357 | + } | |
| 358 | + } | |
| 359 | +} | |
| 360 | + | |
| 361 | +function normalizeLicense(packageJson: PackageJson): string | null { | |
| 362 | + if (typeof packageJson.license === "string" && packageJson.license.trim().length > 0) { | |
| 363 | + return packageJson.license.trim(); | |
| 364 | + } | |
| 365 | + if (isRecord(packageJson.license) && typeof packageJson.license.type === "string") { | |
| 366 | + return packageJson.license.type.trim() || null; | |
| 367 | + } | |
| 368 | + if (Array.isArray(packageJson.licenses)) { | |
| 369 | + const licenses = packageJson.licenses | |
| 370 | + .map((entry) => { | |
| 371 | + if (typeof entry === "string") return entry.trim(); | |
| 372 | + if (isRecord(entry) && typeof entry.type === "string") return entry.type.trim(); | |
| 373 | + return ""; | |
| 374 | + }) | |
| 375 | + .filter((entry) => entry.length > 0); | |
| 376 | + if (licenses.length > 0) return licenses.join(" OR "); | |
| 377 | + } | |
| 378 | + return null; | |
| 379 | +} | |
| 380 | + | |
| 381 | +function normalizeRepositoryUrl(value: unknown): string | null { | |
| 382 | + const raw = | |
| 383 | + typeof value === "string" | |
| 384 | + ? value | |
| 385 | + : isRecord(value) && typeof value.url === "string" | |
| 386 | + ? value.url | |
| 387 | + : null; | |
| 388 | + if (!raw) return null; | |
| 389 | + const trimmed = raw.trim(); | |
| 390 | + if (trimmed.startsWith("github:")) return `https://github.com/${trimmed.slice(7)}`; | |
| 391 | + const normalized = trimmed | |
| 392 | + .replace(/^git\+ssh:\/\/git@github\.com\//, "https://github.com/") | |
| 393 | + .replace(/^git\+/, "") | |
| 394 | + .replace(/^git@github\.com:/, "https://github.com/") | |
| 395 | + .replace(/^ssh:\/\/(?:git@)?github\.com\//, "https://github.com/") | |
| 396 | + .replace(/^git:\/\/github\.com\//, "https://github.com/") | |
| 397 | + .replace(/\.git$/, ""); | |
| 398 | + if (/^[\w.-]+\/[\w.-]+$/.test(normalized)) return `https://github.com/${normalized}`; | |
| 399 | + return normalized; | |
| 400 | +} | |
| 401 | + | |
| 402 | +function packageSourceUrl(packageJson: PackageJson): string | null { | |
| 403 | + if (typeof packageJson.homepage === "string" && packageJson.homepage.trim().length > 0) { | |
| 404 | + return packageJson.homepage.trim(); | |
| 405 | + } | |
| 406 | + return normalizeRepositoryUrl(packageJson.repository); | |
| 407 | +} | |
| 408 | + | |
| 409 | +async function readPackageNoticeText(packageRoot: string): Promise<string | null> { | |
| 410 | + const noticeFiles: string[] = []; | |
| 411 | + const rootEntries = await NodeFSP.readdir(packageRoot, { withFileTypes: true }); | |
| 412 | + noticeFiles.push( | |
| 413 | + ...rootEntries | |
| 414 | + .filter((entry) => entry.isFile() && NOTICE_FILE_PATTERN.test(entry.name)) | |
| 415 | + .map((entry) => entry.name), | |
| 416 | + ); | |
| 417 | + | |
| 418 | + const collectNestedNoticeFiles = async (directory: string, depth: number): Promise<void> => { | |
| 419 | + const directoryEntries = await NodeFSP.readdir(NodePath.join(packageRoot, directory), { | |
| 420 | + withFileTypes: true, | |
| 421 | + }); | |
| 422 | + await Promise.all( | |
| 423 | + directoryEntries.map(async (entry) => { | |
| 424 | + const relativePath = NodePath.join(directory, entry.name); | |
| 425 | + if (entry.isFile() && NOTICE_FILE_PATTERN.test(entry.name)) { | |
| 426 | + noticeFiles.push(relativePath); | |
| 427 | + return; | |
| 428 | + } | |
| 429 | + if ( | |
| 430 | + depth > 0 && | |
| 431 | + entry.isDirectory() && | |
| 432 | + entry.name !== "node_modules" && | |
| 433 | + entry.name !== ".git" | |
| 434 | + ) { | |
| 435 | + await collectNestedNoticeFiles(relativePath, depth - 1); | |
| 436 | + } | |
| 437 | + }), | |
| 438 | + ); | |
| 439 | + }; | |
| 440 | + if (noticeFiles.length === 0) { | |
| 441 | + await Promise.all( | |
| 442 | + rootEntries | |
| 443 | + .filter( | |
| 444 | + (entry) => entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git", | |
| 445 | + ) | |
| 446 | + .map((entry) => collectNestedNoticeFiles(entry.name, 2)), | |
| 447 | + ); | |
| 448 | + } | |
| 449 | + noticeFiles.sort((left, right) => left.localeCompare(right)); | |
| 450 | + if (noticeFiles.length === 0) return null; | |
| 451 | + | |
| 452 | + const sections: string[] = []; | |
| 453 | + for (const fileName of noticeFiles) { | |
| 454 | + const contents = (await NodeFSP.readFile(NodePath.join(packageRoot, fileName), "utf8")).trim(); | |
| 455 | + if (contents.length === 0) continue; | |
| 456 | + sections.push(noticeFiles.length === 1 ? contents : `${fileName}\n\n${contents}`); | |
| 457 | + } | |
| 458 | + return sections.length > 0 ? sections.join("\n\n---\n\n") : null; | |
| 459 | +} | |
| 460 | + | |
| 461 | +function repositoryNoticeKey(packageJson: PackageJson, license: string): string | null { | |
| 462 | + const repositoryUrl = normalizeRepositoryUrl(packageJson.repository); | |
| 463 | + return repositoryUrl | |
| 464 | + ? `${repositoryUrl.toLocaleLowerCase()}\n${license.toLocaleLowerCase()}` | |
| 465 | + : null; | |
| 466 | +} | |
| 467 | + | |
| 468 | +async function collectRepositoryNotices( | |
| 469 | + collection: PackageCollection, | |
| 470 | + packageNotices: Map<string, Promise<string | null>>, | |
| 471 | +): Promise<ReadonlyMap<string, string>> { | |
| 472 | + const notices = new Map<string, string>(); | |
| 473 | + await Promise.all( | |
| 474 | + [...collection.byIdentity.values()].map(async (collected) => { | |
| 475 | + const license = normalizeLicense(collected.packageJson); | |
| 476 | + if (!license) return; | |
| 477 | + const key = repositoryNoticeKey(collected.packageJson, license); | |
| 478 | + if (!key || notices.has(key)) return; | |
| 479 | + const noticeText = await packageNoticeText(collected.packageRoot, packageNotices); | |
| 480 | + if (noticeText) notices.set(key, noticeText); | |
| 481 | + }), | |
| 482 | + ); | |
| 483 | + return notices; | |
| 484 | +} | |
| 485 | + | |
| 486 | +function packageNoticeText( | |
| 487 | + packageRoot: string, | |
| 488 | + cache: Map<string, Promise<string | null>>, | |
| 489 | +): Promise<string | null> { | |
| 490 | + const existing = cache.get(packageRoot); | |
| 491 | + if (existing) return existing; | |
| 492 | + const notice = readPackageNoticeText(packageRoot); | |
| 493 | + cache.set(packageRoot, notice); | |
| 494 | + return notice; | |
| 495 | +} | |
| 496 | + | |
| 497 | +function findPackageOverride( | |
| 498 | + overrides: ReadonlyArray<PackageNoticeOverrideConfigEntry>, | |
| 499 | + name: string, | |
| 500 | + version: string, | |
| 501 | + packageJson: PackageJson, | |
| 502 | +): PackageNoticeOverrideConfigEntry | undefined { | |
| 503 | + const matchingVersion = (override: PackageNoticeOverrideConfigEntry) => | |
| 504 | + override.version === undefined || override.version === version; | |
| 505 | + const repositoryUrl = normalizeRepositoryUrl(packageJson.repository)?.toLocaleLowerCase(); | |
| 506 | + return ( | |
| 507 | + overrides.find((override) => override.name === name && override.version === version) ?? | |
| 508 | + overrides.find((override) => override.name === name && override.version === undefined) ?? | |
| 509 | + overrides.find( | |
| 510 | + (override) => | |
| 511 | + repositoryUrl !== undefined && | |
| 512 | + override.repositoryUrl !== undefined && | |
| 513 | + normalizeRepositoryUrl(override.repositoryUrl)?.toLocaleLowerCase() === repositoryUrl && | |
| 514 | + matchingVersion(override), | |
| 515 | + ) | |
| 516 | + ); | |
| 517 | +} | |
| 518 | + | |
| 519 | +async function packageEntry( | |
| 520 | + collected: CollectedPackage, | |
| 521 | + config: ThirdPartyLicensesConfig, | |
| 522 | + configDirectory: string, | |
| 523 | + packageNotices: Map<string, Promise<string | null>>, | |
| 524 | + repositoryNotices: ReadonlyMap<string, string>, | |
| 525 | +): Promise<ThirdPartyLicenseEntry> { | |
| 526 | + const name = | |
| 527 | + typeof collected.packageJson.name === "string" | |
| 528 | + ? collected.packageJson.name | |
| 529 | + : NodePath.basename(collected.packageRoot); | |
| 530 | + const version = | |
| 531 | + typeof collected.packageJson.version === "string" ? collected.packageJson.version : "unknown"; | |
| 532 | + const override = findPackageOverride( | |
| 533 | + config.packageOverrides, | |
| 534 | + name, | |
| 535 | + version, | |
| 536 | + collected.packageJson, | |
| 537 | + ); | |
| 538 | + const license = override?.license ?? normalizeLicense(collected.packageJson); | |
| 539 | + if (!license || /^(?:unlicensed|proprietary)$/i.test(license)) { | |
| 540 | + throw new Error( | |
| 541 | + `${name}@${version} does not declare a distributable license. Add a package override in the third-party license config if the package publishes its notice elsewhere.`, | |
| 542 | + ); | |
| 543 | + } | |
| 544 | + | |
| 545 | + const repositoryKey = repositoryNoticeKey(collected.packageJson, license); | |
| 546 | + const noticeText = override?.noticeFile | |
| 547 | + ? ( | |
| 548 | + await NodeFSP.readFile(NodePath.resolve(configDirectory, override.noticeFile), "utf8") | |
| 549 | + ).trim() | |
| 550 | + : ((await packageNoticeText(collected.packageRoot, packageNotices)) ?? | |
| 551 | + (repositoryKey ? repositoryNotices.get(repositoryKey) : undefined)); | |
| 552 | + if (!noticeText) { | |
| 553 | + throw new Error( | |
| 554 | + `${name}@${version} does not include a license or notice file. Add a package override with "noticeFile" in the third-party license config.`, | |
| 555 | + ); | |
| 556 | + } | |
| 557 | + | |
| 558 | + return { | |
| 559 | + bundles: [...collected.bundles].sort((left, right) => left.localeCompare(right)), | |
| 560 | + kind: "package", | |
| 561 | + license, | |
| 562 | + name, | |
| 563 | + noticeText, | |
| 564 | + sourceUrl: override?.sourceUrl ?? packageSourceUrl(collected.packageJson), | |
| 565 | + version, | |
| 566 | + }; | |
| 567 | +} | |
| 568 | + | |
| 569 | +async function customEntries( | |
| 570 | + config: ThirdPartyLicensesConfig, | |
| 571 | + configDirectory: string, | |
| 572 | + includedBundles: ReadonlySet<string>, | |
| 573 | +): Promise<ReadonlyArray<ThirdPartyLicenseEntry>> { | |
| 574 | + return Promise.all( | |
| 575 | + config.customNotices | |
| 576 | + .filter( | |
| 577 | + (notice) => | |
| 578 | + notice.bundles === undefined || | |
| 579 | + notice.bundles.some((bundle) => includedBundles.has(bundle)), | |
| 580 | + ) | |
| 581 | + .map(async (notice) => { | |
| 582 | + const noticeText = ( | |
| 583 | + await NodeFSP.readFile(NodePath.resolve(configDirectory, notice.noticeFile), "utf8") | |
| 584 | + ).trim(); | |
| 585 | + if (noticeText.length === 0) { | |
| 586 | + throw new Error(`Custom third-party notice "${notice.name}" is empty.`); | |
| 587 | + } | |
| 588 | + return { | |
| 589 | + bundles: [...(notice.bundles ?? ["assets"])].sort((left, right) => | |
| 590 | + left.localeCompare(right), | |
| 591 | + ), | |
| 592 | + kind: "custom" as const, | |
| 593 | + license: notice.license, | |
| 594 | + name: notice.name, | |
| 595 | + noticeText, | |
| 596 | + sourceUrl: notice.sourceUrl ?? null, | |
| 597 | + version: notice.version ?? null, | |
| 598 | + }; | |
| 599 | + }), | |
| 600 | + ); | |
| 601 | +} | |
| 602 | + | |
| 603 | +function entrySort(left: ThirdPartyLicenseEntry, right: ThirdPartyLicenseEntry): number { | |
| 604 | + return ( | |
| 605 | + left.name.localeCompare(right.name) || | |
| 606 | + (left.version ?? "").localeCompare(right.version ?? "") || | |
| 607 | + left.kind.localeCompare(right.kind) | |
| 608 | + ); | |
| 609 | +} | |
| 610 | + | |
| 611 | +export async function generateThirdPartyLicenseManifest(input: { | |
| 612 | + readonly configFile?: string | URL; | |
| 613 | + readonly packageManifests: ReadonlyArray<ThirdPartyLicensePackageManifest>; | |
| 614 | + readonly bundledModuleIds?: ReadonlyArray<string>; | |
| 615 | + readonly bundleName?: string; | |
| 616 | +}): Promise<ThirdPartyLicenseManifest> { | |
| 617 | + const [{ config, directory }, collection] = await Promise.all([ | |
| 618 | + readConfig(input.configFile), | |
| 619 | + collectProductionDependencyPackages(input.packageManifests), | |
| 620 | + ]); | |
| 621 | + if (input.bundledModuleIds && input.bundleName) { | |
| 622 | + await addBundledModulePackages(collection, input.bundledModuleIds, input.bundleName); | |
| 623 | + } | |
| 624 | + | |
| 625 | + const packageNotices = new Map<string, Promise<string | null>>(); | |
| 626 | + const repositoryNotices = await collectRepositoryNotices(collection, packageNotices); | |
| 627 | + | |
| 628 | + const packageEntryResults = await Promise.allSettled( | |
| 629 | + [...collection.byIdentity.values()].map((collected) => | |
| 630 | + packageEntry(collected, config, directory, packageNotices, repositoryNotices), | |
| 631 | + ), | |
| 632 | + ); | |
| 633 | + const failures = packageEntryResults.flatMap((result) => | |
| 634 | + result.status === "rejected" | |
| 635 | + ? [result.reason instanceof Error ? result.reason.message : String(result.reason)] | |
| 636 | + : [], | |
| 637 | + ); | |
| 638 | + if (failures.length > 0) { | |
| 639 | + throw new Error( | |
| 640 | + `Third-party license generation found ${String(failures.length)} invalid package notice${failures.length === 1 ? "" : "s"}:\n${failures.map((failure) => `- ${failure}`).join("\n")}`, | |
| 641 | + ); | |
| 642 | + } | |
| 643 | + const packageEntries = packageEntryResults.flatMap((result) => | |
| 644 | + result.status === "fulfilled" ? [result.value] : [], | |
| 645 | + ); | |
| 646 | + const includedBundles = new Set(input.packageManifests.map((manifest) => manifest.bundle)); | |
| 647 | + if (input.bundleName) includedBundles.add(input.bundleName); | |
| 648 | + const manualEntries = await customEntries(config, directory, includedBundles); | |
| 649 | + return { | |
| 650 | + schemaVersion: 1, | |
| 651 | + entries: [...packageEntries, ...manualEntries].sort(entrySort), | |
| 652 | + }; | |
| 653 | +} | |
| 654 | + | |
| 655 | +function moduleIdsFromBundle(bundle: unknown): ReadonlyArray<string> { | |
| 656 | + const ids = new Set<string>(); | |
| 657 | + if (!isRecord(bundle)) return []; | |
| 658 | + for (const output of Object.values(bundle)) { | |
| 659 | + if (!isRecord(output) || output.type !== "chunk" || !isRecord(output.modules)) continue; | |
| 660 | + for (const id of Object.keys(output.modules)) ids.add(id); | |
| 661 | + } | |
| 662 | + return [...ids]; | |
| 663 | +} | |
| 664 | + | |
| 665 | +function serializeManifest(manifest: ThirdPartyLicenseManifest): string { | |
| 666 | + return `${JSON.stringify(manifest)}\n`; | |
| 667 | +} | |
| 668 | + | |
| 669 | +export function thirdPartyLicensesPlugin(options: ThirdPartyLicensesPluginOptions): Plugin { | |
| 670 | + return { | |
| 671 | + name: "t3code:third-party-licenses", | |
| 672 | + configureServer(server) { | |
| 673 | + let manifestPromise: Promise<ThirdPartyLicenseManifest> | null = null; | |
| 674 | + server.middlewares.use((request, response, next) => { | |
| 675 | + if (request.url?.split("?", 1)[0] !== `/${THIRD_PARTY_LICENSES_FILE_NAME}`) { | |
| 676 | + next(); | |
| 677 | + return; | |
| 678 | + } | |
| 679 | + manifestPromise ??= generateThirdPartyLicenseManifest({ | |
| 680 | + packageManifests: options.packageManifests, | |
| 681 | + ...(options.configFile !== undefined ? { configFile: options.configFile } : {}), | |
| 682 | + }); | |
| 683 | + void manifestPromise.then( | |
| 684 | + (manifest) => { | |
| 685 | + response.statusCode = 200; | |
| 686 | + response.setHeader("Content-Type", "application/json; charset=utf-8"); | |
| 687 | + response.setHeader("Cache-Control", "no-store"); | |
| 688 | + response.end(serializeManifest(manifest)); | |
| 689 | + }, | |
| 690 | + (error: unknown) => { | |
| 691 | + next(error instanceof Error ? error : new Error(String(error))); | |
| 692 | + }, | |
| 693 | + ); | |
| 694 | + }); | |
| 695 | + }, | |
| 696 | + async generateBundle(_outputOptions, bundle) { | |
| 697 | + const manifest = await generateThirdPartyLicenseManifest({ | |
| 698 | + packageManifests: options.packageManifests, | |
| 699 | + bundledModuleIds: moduleIdsFromBundle(bundle), | |
| 700 | + bundleName: options.bundleName, | |
| 701 | + ...(options.configFile !== undefined ? { configFile: options.configFile } : {}), | |
| 702 | + }); | |
| 703 | + this.emitFile({ | |
| 704 | + type: "asset", | |
| 705 | + fileName: THIRD_PARTY_LICENSES_FILE_NAME, | |
| 706 | + source: serializeManifest(manifest), | |
| 707 | + }); | |
| 708 | + }, | |
| 709 | + }; | |
| 710 | +} | |
| @@ -0,0 +1,181 @@ | ||
| 1 | +{ | |
| 2 | + "customNotices": [ | |
| 3 | + { | |
| 4 | + "bundles": ["assets", "mobile", "web"], | |
| 5 | + "license": "MIT", | |
| 6 | + "name": "vscode-icons", | |
| 7 | + "noticeFile": "apps/web/THIRD_PARTY_NOTICES.md", | |
| 8 | + "sourceUrl": "https://github.com/vscode-icons/vscode-icons" | |
| 9 | + }, | |
| 10 | + { | |
| 11 | + "bundles": ["android", "assets", "mobile", "web"], | |
| 12 | + "license": "MIT", | |
| 13 | + "name": "libghostty-vt", | |
| 14 | + "noticeFile": "native/libghostty-vt/LICENSE", | |
| 15 | + "sourceUrl": "https://github.com/ghostty-org/ghostty" | |
| 16 | + }, | |
| 17 | + { | |
| 18 | + "bundles": ["assets", "web"], | |
| 19 | + "license": "MIT", | |
| 20 | + "name": "Symbols Nerd Font Mono", | |
| 21 | + "noticeFile": "apps/web/src/terminal/ghostty/fonts/LICENSE", | |
| 22 | + "sourceUrl": "https://github.com/ryanoasis/nerd-fonts" | |
| 23 | + }, | |
| 24 | + { | |
| 25 | + "bundles": ["ios", "mobile"], | |
| 26 | + "license": "MIT", | |
| 27 | + "name": "GhosttyKit", | |
| 28 | + "noticeFile": "apps/mobile/licenses/ghostty-kit.txt", | |
| 29 | + "sourceUrl": "https://github.com/Yash-Singh1/ghostty/tree/custom-io" | |
| 30 | + }, | |
| 31 | + { | |
| 32 | + "bundles": ["android", "assets", "mobile"], | |
| 33 | + "license": "Apache-2.0", | |
| 34 | + "name": "MesloLGS NF", | |
| 35 | + "noticeFile": "apps/mobile/licenses/meslo-lgs-nf.txt", | |
| 36 | + "sourceUrl": "https://github.com/romkatv/powerlevel10k-media" | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + "bundles": ["mobile"], | |
| 40 | + "license": "MIT", | |
| 41 | + "name": "react-native-uitextview", | |
| 42 | + "noticeFile": "apps/mobile/modules/t3-markdown-text/LICENSE", | |
| 43 | + "sourceUrl": "https://github.com/bluesky-social/react-native-uitextview", | |
| 44 | + "version": "2.2.0" | |
| 45 | + }, | |
| 46 | + { | |
| 47 | + "bundles": ["mobile"], | |
| 48 | + "license": "MIT", | |
| 49 | + "name": "Expo text editor implementation", | |
| 50 | + "noticeFile": "apps/mobile/modules/t3-composer-editor/LICENSE", | |
| 51 | + "sourceUrl": "https://github.com/expo/expo" | |
| 52 | + } | |
| 53 | + ], | |
| 54 | + "packageOverrides": [ | |
| 55 | + { | |
| 56 | + "license": "MIT", | |
| 57 | + "name": "@react-grab/cli", | |
| 58 | + "noticeFile": "apps/web/licenses/react-grab-cli.txt", | |
| 59 | + "sourceUrl": "https://github.com/aidenybai/react-grab/tree/main/packages/cli" | |
| 60 | + }, | |
| 61 | + { | |
| 62 | + "name": "glob-to-regexp", | |
| 63 | + "noticeFile": "apps/web/licenses/glob-to-regexp.txt" | |
| 64 | + }, | |
| 65 | + { | |
| 66 | + "name": "kubernetes-types", | |
| 67 | + "noticeFile": "apps/web/licenses/kubernetes-types.txt" | |
| 68 | + }, | |
| 69 | + { | |
| 70 | + "name": "@msgpackr-extract/msgpackr-extract-linux-x64", | |
| 71 | + "noticeFile": "apps/web/licenses/msgpackr-extract-linux-x64.txt" | |
| 72 | + }, | |
| 73 | + { | |
| 74 | + "name": "isarray", | |
| 75 | + "noticeFile": "apps/web/licenses/isarray.txt" | |
| 76 | + }, | |
| 77 | + { | |
| 78 | + "name": "keyv", | |
| 79 | + "noticeFile": "apps/web/licenses/keyv.txt" | |
| 80 | + }, | |
| 81 | + { | |
| 82 | + "license": "MIT", | |
| 83 | + "name": "type-fest", | |
| 84 | + "noticeFile": "apps/web/licenses/type-fest.txt" | |
| 85 | + }, | |
| 86 | + { | |
| 87 | + "name": "lazy-val", | |
| 88 | + "noticeFile": "apps/web/licenses/lazy-val.txt" | |
| 89 | + }, | |
| 90 | + { | |
| 91 | + "name": "@pierre/theming", | |
| 92 | + "noticeFile": "apps/web/licenses/pierre-theming.txt" | |
| 93 | + }, | |
| 94 | + { | |
| 95 | + "name": "lru_map", | |
| 96 | + "noticeFile": "apps/web/licenses/lru-map.txt" | |
| 97 | + }, | |
| 98 | + { | |
| 99 | + "name": "@electron-internal/extract-zip", | |
| 100 | + "noticeFile": "apps/web/licenses/electron-internal-extract-zip.txt" | |
| 101 | + }, | |
| 102 | + { | |
| 103 | + "noticeFile": "apps/mobile/licenses/metro.txt", | |
| 104 | + "repositoryUrl": "https://github.com/facebook/metro" | |
| 105 | + }, | |
| 106 | + { | |
| 107 | + "noticeFile": "apps/mobile/licenses/metro.txt", | |
| 108 | + "repositoryUrl": "https://github.com/react/metro" | |
| 109 | + }, | |
| 110 | + { | |
| 111 | + "name": "@react-native-ai/apple", | |
| 112 | + "noticeFile": "apps/mobile/licenses/react-native-ai.txt", | |
| 113 | + "sourceUrl": "https://github.com/callstackincubator/ai" | |
| 114 | + }, | |
| 115 | + { | |
| 116 | + "name": "react-remove-scroll-bar", | |
| 117 | + "noticeFile": "apps/mobile/licenses/react-remove-scroll-bar.txt" | |
| 118 | + }, | |
| 119 | + { | |
| 120 | + "name": "standard-navigation", | |
| 121 | + "noticeFile": "apps/mobile/licenses/standard-navigation.txt" | |
| 122 | + }, | |
| 123 | + { | |
| 124 | + "name": "@expo/sdk-runtime-versions", | |
| 125 | + "noticeFile": "apps/mobile/modules/t3-composer-editor/LICENSE" | |
| 126 | + }, | |
| 127 | + { | |
| 128 | + "name": "@expo/ws-tunnel", | |
| 129 | + "noticeFile": "apps/mobile/modules/t3-composer-editor/LICENSE" | |
| 130 | + }, | |
| 131 | + { | |
| 132 | + "name": "stream-buffers", | |
| 133 | + "noticeFile": "apps/mobile/licenses/stream-buffers.txt" | |
| 134 | + }, | |
| 135 | + { | |
| 136 | + "name": "bplist-parser", | |
| 137 | + "noticeFile": "apps/mobile/licenses/bplist-parser.txt" | |
| 138 | + }, | |
| 139 | + { | |
| 140 | + "name": "@expo/devcert", | |
| 141 | + "noticeFile": "apps/mobile/licenses/expo-devcert.txt" | |
| 142 | + }, | |
| 143 | + { | |
| 144 | + "name": "jimp-compact", | |
| 145 | + "noticeFile": "apps/mobile/licenses/jimp-compact.txt" | |
| 146 | + }, | |
| 147 | + { | |
| 148 | + "name": "fb-watchman", | |
| 149 | + "noticeFile": "apps/web/licenses/kubernetes-types.txt" | |
| 150 | + }, | |
| 151 | + { | |
| 152 | + "name": "bser", | |
| 153 | + "noticeFile": "apps/web/licenses/kubernetes-types.txt" | |
| 154 | + }, | |
| 155 | + { | |
| 156 | + "name": "@expo/xcpretty", | |
| 157 | + "noticeFile": "apps/mobile/licenses/expo-xcpretty.txt" | |
| 158 | + }, | |
| 159 | + { | |
| 160 | + "license": "MIT", | |
| 161 | + "name": "fb-dotslash", | |
| 162 | + "noticeFile": "apps/mobile/licenses/fb-dotslash.txt" | |
| 163 | + }, | |
| 164 | + { | |
| 165 | + "name": "structured-headers", | |
| 166 | + "noticeFile": "apps/mobile/licenses/structured-headers.txt" | |
| 167 | + }, | |
| 168 | + { | |
| 169 | + "name": "badgin", | |
| 170 | + "noticeFile": "apps/mobile/licenses/badgin.txt" | |
| 171 | + }, | |
| 172 | + { | |
| 173 | + "name": "react-native-nitro-modules", | |
| 174 | + "noticeFile": "apps/mobile/licenses/react-native-nitro-modules.txt" | |
| 175 | + }, | |
| 176 | + { | |
| 177 | + "name": "boolbase", | |
| 178 | + "noticeFile": "apps/mobile/licenses/boolbase.txt" | |
| 179 | + } | |
| 180 | + ] | |
| 181 | +} | |