feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,88 @@
// (deprecated because unused)
// * Creates a pull request in the github repository from a local "metadata-update" branch.
// * Deletes the "metadata-update" branch.
// In order for this script to work:
//
// * Install `hub` command line tool: https://hub.github.com/
// * Create a "Personal Access Token" in GitHub account settings (just "repo_public" would be enough)
// * Tell `hub` to use the token for creating GitHub pull requests: `echo "---\ngithub.com:\n- protocol: https\n user: GITHUB_USERNAME\n oauth_token: TOKEN" >> ~/.config/hub`
import updateMetadataFromGoogleMetadata from './helpers/updateMetadataFromGoogleMetadata.js'
import commit from './helpers/commit.js'
import exec from './helpers/exec.js'
const googleMetadataFilePath = process.argv[2]
const metadataInfoFilePath = process.argv[3]
// This script is meant to be run from a local "update-metadata" branch.
// See `createMetadataUpdateBranch()` function for how it could be set up.
throw new Error('Run `createMetadataUpdateBranch()` function first')
const metadataChanged = updateMetadataFromGoogleMetadata(googleMetadataFilePath, metadataInfoFilePath)
if (metadataChanged) {
commit()
console.log()
console.log('========================================')
console.log('= Pushing changes =')
console.log('========================================')
console.log()
// Delete pre-existing remote `update-metadata` remote branch
// (if it already exists)
if (exec('git ls-remote --heads origin update-metadata'))
{
console.log(exec('git push origin update-metadata --delete'))
}
// Push the local `update-metadata` branch to GitHub
console.log(exec('git push origin update-metadata'))
console.log()
console.log('========================================')
console.log('= Pushed. Creating Pull Request. =')
console.log('========================================')
console.log()
console.log(exec('hub pull-request -m "Updated metadata" -b catamphetamine/libphonenumber-js:master -h update-metadata'))
console.log()
console.log('========================================')
console.log('= Pull Request created =')
console.log('========================================')
console.log()
console.log(exec('git checkout master'))
console.log(exec('git branch -D update-metadata'))
}
// This function creates "update-metadata" branch and switches into it.
function createMetadataUpdateBranch() {
let metadata_branch_exists = false
try
{
exec('git rev-parse --verify update-metadata')
metadata_branch_exists = true
}
catch (error)
{
if (error.message.indexOf('fatal: Needed a single revision') === -1)
{
throw error
}
}
if (metadata_branch_exists)
{
console.log(exec('git checkout master'))
console.log(exec('git branch -D update-metadata'))
}
console.log(exec('git pull'))
console.log(exec('git branch update-metadata origin/master'))
console.log(exec('git checkout update-metadata'))
}

View File

@@ -0,0 +1,67 @@
// Pulls the latest metadata from Google's repo, and, if it has changed,
// pushes the changes in the main branch and releases a new version on `npm`.
import updateMetadataFromGoogleMetadata from './helpers/updateMetadataFromGoogleMetadata.js'
import commit from './helpers/commit.js'
import exec from './helpers/exec.js'
import sendEmail from './helpers/sendEmail.js'
const googleMetadataFilePath = process.argv[2]
const metadataInfoFilePath = process.argv[3]
const metadataChanged = updateMetadataFromGoogleMetadata(googleMetadataFilePath, metadataInfoFilePath)
if (metadataChanged) {
commit()
console.log()
console.log('========================================')
console.log('= Push the changes to the repository =')
console.log('========================================')
console.log()
console.log(exec('git push'))
console.log()
console.log('========================================')
console.log('= Increment the version =')
console.log('========================================')
console.log()
console.log(exec('npm version patch'))
console.log(exec('git push'))
console.log()
console.log('========================================')
console.log('= Publish the new version =')
console.log('========================================')
console.log()
// `npm` requires "two-factor authentication", so running an `npm publish`
// command programmatically won't work without human intervention.
//
// To work around that, one could optionally specify GMail credentials.
// In that case, instead of attempting to run `npm publish` command,
// it will send an email notification to do that manually.
//
if (await sendEmail({
// Send an email notification about manually publishing the new version of the package to `npm`.
subject: '[libphonenumber-js] Publish a new version',
text: 'A new version of `libphonenumber-js` has been pushed to the repository:\nhttps://gitlab.com/catamphetamine/libphonenumber-js/-/commits/master\n\nRun `npm publish` in `libphonenumber-js` directory to publish the new version.'
})) {
console.log()
console.log('==========================================================')
console.log('= Email notification sent. Publish the package manually. =')
console.log('==========================================================')
console.log()
} else {
// Attempt to publish the `npm` package.
console.log(exec('npm publish'))
console.log()
console.log('========================================')
console.log('= Published =')
console.log('========================================')
console.log()
}
}

View File

@@ -0,0 +1,13 @@
import getUncommittedChanges from './helpers/getUncommittedChanges.js';
import sendEmail from './helpers/sendEmail.js'
const uncommittedChanges = await getUncommittedChanges()
if (uncommittedChanges.length > 0) {
const errorText = `You have uncommitted files in your repository:\n\n${uncommittedChanges.map(({ filename }) => filename).join('\n')}`
console.error(new Error(errorText))
await sendEmail({
subject: '[libphonenumber-js] Uncommitted changes found',
text: errorText
})
process.exit(1)
}

View File

@@ -0,0 +1,11 @@
// Creates a `package.json` file in the CommonJS `build` folder.
// That marks that whole folder as CommonJS so that Node.js doesn't complain
// about `require()`-ing those files.
import fs from 'fs'
fs.writeFileSync('./build/package.json', JSON.stringify({
name: 'libphonenumber-js/build',
type: 'commonjs',
private: true
}, null, 2), 'utf8')

View File

@@ -0,0 +1,14 @@
import fs from 'fs'
const COMMENT = '// This file is a workaround for a bug in web browsers\' "native"' + '\n' +
'// ES6 importing system which is uncapable of importing "*.json" files.' + '\n' +
'// https://github.com/catamphetamine/libphonenumber-js/issues/239'
const path = process.argv[2]
createJsFileForJsonFile(path)
function createJsFileForJsonFile(path) {
let contents = fs.readFileSync(path, 'utf-8')
contents = COMMENT + '\n' + 'export default ' + contents
fs.writeFileSync(path + '.js', contents)
}

View File

@@ -0,0 +1,31 @@
// (deprecated because unused)
// Ccreates a new git branch called "update-metadata" and switches into it.
// If the branch with such name already exists, it overwrites it with a new one.
import exec from './helpers/exec.js'
let metadata_branch_exists = false
try
{
exec('git rev-parse --verify update-metadata')
metadata_branch_exists = true
}
catch (error)
{
if (error.message.indexOf('fatal: Needed a single revision') === -1)
{
throw error
}
}
if (metadata_branch_exists)
{
console.log(exec('git checkout master'))
console.log(exec('git branch -D update-metadata'))
}
console.log(exec('git pull'))
console.log(exec('git branch update-metadata origin/master'))
console.log(exec('git checkout update-metadata'))

View File

@@ -0,0 +1,14 @@
import metadata from '../metadata.min.json' with { type: 'json' }
import fs from 'fs'
const countryCodes = Object.keys(metadata.countries)
// Fills in the list of possible values of `CountryCode` type in the TypeScript definition file.
fs.writeFileSync(
'./types.d.ts',
fs.readFileSync('./types.d.ts', 'utf-8').replace(
/export type CountryCode = .*;/,
`export type CountryCode = ${countryCodes.map(_ => `'${_}'`).join(' | ')};`
),
'utf-8'
)

View File

@@ -0,0 +1,75 @@
// Generates JSON metadata from Google's `PhoneNumberMetadata.xml` file.
import minimist from 'minimist'
import fs from 'fs'
import { version, generate, compress } from 'libphonenumber-metadata-generator'
// const REGION_CODE_FOR_NON_GEO_ENTITY = '001'
const input_file = process.argv[2]
const output_file = process.argv[3]
const input = fs.readFileSync(input_file, 'utf8')
const command_line_arguments = minimist(process.argv.slice(4))
// Included countries
let included_countries
if (command_line_arguments.countries) {
included_countries = command_line_arguments.countries.split(',')
console.log('Included countries:', included_countries)
included_countries = new Set(included_countries)
}
// Include all regular expressions
let extended = false
if (command_line_arguments.extended) {
console.log('Include extra validation regular expressions')
extended = true
}
// Included phone number types
let included_phone_number_types
if (command_line_arguments.types) {
included_phone_number_types = command_line_arguments.types.split(',')
console.log('Included phone number types:', included_phone_number_types)
included_phone_number_types = new Set(included_phone_number_types)
}
// Generate and compress metadata
generate(input, version, included_countries, extended, included_phone_number_types).then((output) => {
// Write uncompressed metadata into a file for easier debugging
if (command_line_arguments.debug) {
console.log('Output uncompressed JSON for debugging')
fs.writeFileSync('./metadata.json', JSON.stringify(output, undefined, 3))
}
// Compress the generated metadata
fs.writeFileSync(output_file, JSON.stringify(compress(output)))
// Output mobile phone number type examples
if (command_line_arguments.examples === 'mobile') {
var examples = Object.keys(output.countries).reduce(function(out, country_code) {
// if (country_code === REGION_CODE_FOR_NON_GEO_ENTITY) {
// return out
// }
var mobile = output.countries[country_code].examples.mobile
var fixed_line = output.countries[country_code].examples.fixed_line
if (mobile) {
out[country_code] = mobile
}
// "TA" country doesn't have any mobile phone number example
else if (fixed_line) {
console.warn(`Country ${country_code} doesn't have a mobile phone number example. Substituting with a fixed line phone number example.`)
out[country_code] = fixed_line
} else {
console.error(`Country ${country_code} doesn't have neither a mobile phone number example nor a fixed line phone number example.`)
// `async` errors aren't being caught at the top level in Node.js
process.exit(1)
}
return out
}, {})
fs.writeFileSync('./examples.mobile.json', JSON.stringify(examples))
}
})

View File

@@ -0,0 +1,14 @@
import exec from './exec.js'
// Commits the current changes files in the local git repository.
export default function() {
console.log()
console.log('========================================')
console.log('= Committing changes =')
console.log('========================================')
console.log()
console.log(exec('git add .'))
console.log(exec('git commit -m "Updated metadata"'))
}

View File

@@ -0,0 +1,6 @@
import { execSync } from 'child_process'
// Executes a command.
export default function exec(command) {
return execSync(command).toString().trim()
}

View File

@@ -0,0 +1,33 @@
import { exec } from 'child_process'
export default function getUncommittedChanges() {
return new Promise((resolve, reject) => {
exec('git status --porcelain', (error, stdout, stderr) => {
if (error) {
reject(error);
return
}
if (stderr) {
reject(new Error(stderr.toString()))
return
}
const output = stdout.toString().trim()
if (!output) {
resolve([]);
}
const uncommittedFiles = output.trim().split('\n').map((line) => {
line = line.trim()
const whitespaceIndex = line.indexOf(' ')
const status = line.slice(0, whitespaceIndex)
const filename = line.slice(whitespaceIndex + ' '.length)
return { status, filename }
})
resolve(uncommittedFiles)
})
})
}

View File

@@ -0,0 +1,62 @@
import fs from 'fs'
import gmailSend from 'gmail-send'
export default async function sendEmail({ subject, text }) {
const emailSettings = getEmailSettings()
if (emailSettings) {
// Send an email notification through GMail.
//
// Example output:
//
// {
// result: '250 2.0.0 OK 1759154240 2adb3069b0e04-58313dd67c4sm4173572e87.55 - gsmtp',
// full: {
// accepted: [ 'kuchumovn@gmail.com' ],
// rejected: [],
// ehlo: [
// 'SIZE 35882577',
// '8BITMIME',
// 'AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH',
// 'ENHANCEDSTATUSCODES',
// 'PIPELINING',
// 'CHUNKING',
// 'SMTPUTF8'
// ],
// envelopeTime: 297,
// messageTime: 565,
// messageSize: 799,
// response: '250 2.0.0 OK 1759154240 2adb3069b0e04-58313dd67c4sm4173572e87.55 - gsmtp',
// envelope: { from: 'kuchumovn@gmail.com', to: [Array] },
// messageId: '<b7c49e37-1f0f-5c42-13c5-2398a778abd2@gmail.com>'
// }
// }
//
const { result, full } = await gmailSend({
user: emailSettings.email,
pass: emailSettings.accessToken,
to: emailSettings.email,
subject,
text
// html: '<b>html text</b>' // alternative to specifying `text`, one could specify `html`.
})()
return true
}
}
// Reads email settings from a JSON file just outside of `libphonenumber-js` repo folder.
function getEmailSettings() {
const emailSettingsPath = '../libphonenumber-js-autoupdate-email-notification-settings.json'
if (fs.existsSync(emailSettingsPath)) {
const emailSettings = JSON.parse(fs.readFileSync(emailSettingsPath, 'utf-8'))
// Validate `emailSettings`.
if (!emailSettings.email) {
throw new Error('`email` parameter not specified in the email config file')
}
// The instructions on how to obtain an `accessToken` are described in `gmail-send` readme:
// https://www.npmjs.com/package/gmail-send
if (!emailSettings.accessToken) {
throw new Error('`accessToken` parameter not specified in the email config file')
}
return emailSettings;
}
}

View File

@@ -0,0 +1,130 @@
import fs from 'fs'
import semver from 'semver'
import exec from './exec.js'
// Checks if Google's metadata XML file has changes, and, if it has,
// generates JSON metadata files from it, runs the tests and updates `CHANGELOG.md`.
export default function(googleMetadataFilePath, metadataInfoFilePath) {
let metadataChanged = exec(`git ls-files --modified ${googleMetadataFilePath}`)
if (!metadataChanged) {
console.log()
console.log('========================================')
console.log('= Metadata is up-to-date. Exiting. =')
console.log('========================================')
console.log()
return
}
console.log()
console.log('========================================')
console.log('= Metadata has changed, updating files =')
console.log('========================================')
console.log()
console.log(exec('npm run metadata:generate'))
console.log()
console.log('========================================')
console.log('= Running tests =')
console.log('========================================')
console.log()
// console.log('* Actually not running tests because if they fail then it won\'t be reported in any way, and if instead tests fail for the Pull Request on github then the repo owner will be notified by Travis CI about that.')
console.log(exec('npm run build'))
console.log(exec('npm test'))
let modified_files = exec('git ls-files --modified').split(/\s/)
let unexpected_modified_files = modified_files.filter((file) => {
return file !== googleMetadataFilePath &&
!/^metadata\.[a-z]+\.json$/.test(file) &&
!/^examples\.[a-z]+\.json$/.test(file)
})
// Turned off this "modified files" check
// because on Windows random files constantly got "modified"
// without actually being modified.
// (perhaps something related to line endings)
if (false && unexpected_modified_files.length > 0) {
let error
error += 'Only `' + googleMetadataFilePath + '`, `metadata.*.json` and `examples.*.json` files should be modified. Unexpected modified files:'
error += '\n'
error += '\n'
error += unexpected_modified_files.join('\n')
console.log()
console.log('========================================')
console.log('= Error =')
console.log('========================================')
console.log()
console.log(error)
throw new Error(error)
}
// Doesn't work
//
// // http://stackoverflow.com/questions/33610682/git-list-of-staged-files
// let staged_files = exec('git diff --name-only --cached').split(/\s/)
//
// if (staged_files.length > 0)
// {
// console.log()
// console.log('========================================')
// console.log('= Error =')
// console.log('========================================')
// console.log()
// console.log('There are some staged files already. Aborting metadata update process.')
// console.log()
// console.log(staged_files.join('\n'))
//
// process.exit(1)
// }
// Add an entry in `CHANGELOG.md`.
addMetadataUpdateChangelogEntry(metadataInfoFilePath)
return true
}
function addMetadataUpdateChangelogEntry(metadataInfoFilePath) {
const {
version: metadataVersion,
changes: metadataChanges
} = JSON.parse(fs.readFileSync(metadataInfoFilePath, 'utf8'))
const packageVersion = JSON.parse(fs.readFileSync('./package.json', 'utf8')).version
const nextPackageVersion = semver.inc(packageVersion, 'patch')
const now = new Date()
let changesLog = ''
changesLog += `${nextPackageVersion} / ${now.getDate()}.${now.getMonth() + 1}.${now.getFullYear()}`
changesLog += '\n'
changesLog += '==================='
changesLog += '\n'
changesLog += '\n'
changesLog += `* Updated metadata to version ${metadataVersion}`
if (metadataChanges.length > 0) {
changesLog += ':'
changesLog += '\n'
changesLog += metadataChanges.map(change => ` - ${change.replace(/\n/g, '\n' + ' ')}`).join('\n')
}
const changelog = fs.readFileSync('./CHANGELOG.md', 'utf8')
const changesLogStartMarker = '<!-- CHANGELOG START -->'
const changesLogStartMarkerStartsAt = changelog.indexOf(changesLogStartMarker)
if (changesLogStartMarkerStartsAt < 0) {
throw new Error('Changelog start marker not found in CHANGELOG.md')
}
const changesLogStartsAt = changesLogStartMarkerStartsAt + changesLogStartMarker.length
const changelogPre = changelog.slice(0, changesLogStartsAt)
const previousChangesLog = changelog.slice(changesLogStartsAt).trim()
fs.writeFileSync('./CHANGELOG.md', changelogPre + '\n\n' + changesLog + '\n\n' + previousChangesLog)
}

View File

@@ -0,0 +1,29 @@
import fs from 'fs'
import { download } from 'libphonenumber-metadata-generator'
const metadataFileOutputPath = process.argv[2]
const infoFileOutputPath = process.argv[3]
// Pulls the latest released metadata from Google's repostory.
// Updates `PhoneNumberMetadata.xml` and `metadata-info.json` files.
download().then(({ date, version, changes, xml }) => {
// Print the metadata version and a list of changes.
console.log('========================================')
console.log(`= Metadata / ${version}`)
console.log('=')
console.log(`= Date: ${date.toUTCString()
.slice(0, date.toUTCString().indexOf('00:00:00') - 1)
.slice(date.toUTCString().indexOf(',') + ', '.length)
}`)
console.log('=')
const consoleOutputPrefix = '= '
console.log(`= Changes:\n${changes.map(_ => consoleOutputPrefix + '* ' + _.replace(/\n/g, '\n' + consoleOutputPrefix + ' ')).join('\n')}`)
console.log('========================================')
// Write metadata changes info.
fs.writeFileSync(infoFileOutputPath, JSON.stringify({ date, version, changes }, null, 2))
// Write the latest metadata XML to `PhoneNumberMetadata.xml` file.
fs.writeFileSync(metadataFileOutputPath, xml)
}).catch((error) => {
console.error(error)
process.exit(1)
})