move esbuild and package.json to root
This commit is contained in:
Родитель
7cab18d0f4
Коммит
3c50bc296b
|
@ -1,85 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
/* eslint-disable no-process-env */
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
require('dotenv').config({ path: path.join(__dirname, '.env') })
|
||||
|
||||
const requiredEnvVars = [
|
||||
'NODE_ENV',
|
||||
'SERVER_URL',
|
||||
'LOGOS_ORIGIN',
|
||||
'COOKIE_SECRET',
|
||||
'SESSION_DURATION_HOURS',
|
||||
'SMTP_URL',
|
||||
'EMAIL_FROM',
|
||||
'SES_CONFIG_SET',
|
||||
'SES_NOTIFICATION_LOG_ONLY',
|
||||
'BREACH_RESOLUTION_ENABLED',
|
||||
'FXA_ENABLED',
|
||||
'FXA_SETTINGS_URL',
|
||||
'FX_REMOTE_SETTINGS_WRITER_SERVER',
|
||||
'FX_REMOTE_SETTINGS_WRITER_USER',
|
||||
'FX_REMOTE_SETTINGS_WRITER_PASS',
|
||||
'MOZLOG_FMT',
|
||||
'MOZLOG_LEVEL',
|
||||
'OAUTH_AUTHORIZATION_URI',
|
||||
'OAUTH_TOKEN_URI',
|
||||
'OAUTH_PROFILE_URI',
|
||||
'OAUTH_CLIENT_ID',
|
||||
'OAUTH_CLIENT_SECRET',
|
||||
'HIBP_KANON_API_ROOT',
|
||||
'HIBP_KANON_API_TOKEN',
|
||||
'HIBP_API_ROOT',
|
||||
'HIBP_RELOAD_BREACHES_TIMER',
|
||||
'HIBP_THROTTLE_DELAY',
|
||||
'HIBP_THROTTLE_MAX_TRIES',
|
||||
'HIBP_NOTIFY_TOKEN',
|
||||
'DATABASE_URL',
|
||||
'PAGE_TOKEN_TIMER',
|
||||
'PRODUCT_PROMOS_ENABLED',
|
||||
'REDIS_URL',
|
||||
'SENTRY_DSN_LEGACY',
|
||||
'DELETE_UNVERIFIED_SUBSCRIBERS_TIMER',
|
||||
'EXPERIMENT_ACTIVE',
|
||||
'MAX_NUM_ADDRESSES'
|
||||
]
|
||||
|
||||
const optionalEnvVars = [
|
||||
'PORT',
|
||||
'RECRUITMENT_BANNER_LINK',
|
||||
'RECRUITMENT_BANNER_TEXT',
|
||||
'GEOIP_GEOLITE2_PATH',
|
||||
'GEOIP_GEOLITE2_CITY_FILENAME',
|
||||
'GEOIP_GEOLITE2_COUNTRY_FILENAME',
|
||||
'VPN_PROMO_BLOCKED_LOCALES',
|
||||
'EDUCATION_VIDEO_URL_RELAY',
|
||||
'EDUCATION_VIDEO_URL_VPN',
|
||||
'ADMINS',
|
||||
'MONTHLY_CRON_ENABLED',
|
||||
'MONTHLY_CRON_LIMIT'
|
||||
]
|
||||
|
||||
const AppConstants = { }
|
||||
|
||||
if (!process.env.SERVER_URL && process.env.NODE_ENV === 'heroku') {
|
||||
process.env.SERVER_URL = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
|
||||
}
|
||||
|
||||
for (const v of requiredEnvVars) {
|
||||
if (process.env[v] === undefined) {
|
||||
throw new Error(`Required environment variable was not set: ${v}`)
|
||||
}
|
||||
AppConstants[v] = process.env[v]
|
||||
}
|
||||
|
||||
optionalEnvVars.forEach(key => {
|
||||
if (key in process.env) AppConstants[key] = process.env[key]
|
||||
})
|
||||
|
||||
AppConstants.VPN_PROMO_BLOCKED_LOCALES = AppConstants.VPN_PROMO_BLOCKED_LOCALES?.split(',')
|
||||
|
||||
AppConstants.AD_UNIT_TOTAL = fs.readdirSync(path.join(__dirname, 'views/partials/ad-units')).length
|
||||
|
||||
module.exports = Object.freeze(AppConstants)
|
|
@ -2,20 +2,18 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// TODO: this is a build/config file and should probably live in the root, after the old esbuild.js file is removed.
|
||||
|
||||
// ESBuild is used to concat and compress the 'client' folder into the 'dist' folder (front-end only)
|
||||
|
||||
import esbuild from 'esbuild'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import AppConstants from './appConstants.js'
|
||||
import AppConstants from './src/appConstants.js'
|
||||
|
||||
const cssPartialDir = 'client/css/partials/'
|
||||
const cssPartialDir = 'src/client/css/partials/'
|
||||
const cssPartialPaths = readdirSync(cssPartialDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile())
|
||||
.map(dirent => cssPartialDir + dirent.name)
|
||||
|
||||
const jsPartialDir = 'client/js/partials/'
|
||||
const jsPartialDir = 'src/client/js/partials/'
|
||||
const jsPartialPaths = readdirSync(jsPartialDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile())
|
||||
.map(dirent => jsPartialDir + dirent.name)
|
||||
|
@ -23,12 +21,12 @@ const jsPartialPaths = readdirSync(jsPartialDir, { withFileTypes: true })
|
|||
esbuild.build({
|
||||
logLevel: 'info',
|
||||
bundle: true,
|
||||
entryPoints: ['client/js/index.js', 'client/css/index.css', ...cssPartialPaths, ...jsPartialPaths],
|
||||
entryPoints: ['src/client/js/index.js', 'src/client/css/index.css', ...cssPartialPaths, ...jsPartialPaths],
|
||||
entryNames: '[dir]/[name]',
|
||||
loader: { '.woff2': 'copy' },
|
||||
assetNames: '[dir]/[name]',
|
||||
external: ['*.webp', '*.svg'],
|
||||
outdir: '../dist',
|
||||
outdir: 'dist',
|
||||
format: 'esm',
|
||||
minify: AppConstants.NODE_ENV !== 'dev',
|
||||
sourcemap: AppConstants.NODE_ENV !== 'dev',
|
||||
|
@ -48,6 +46,6 @@ ESBuild automatic code-splitting is disabled for the following reasons:
|
|||
- As of this writing, ESBuild code-splitting is suggested to be experimental/WIP: https://esbuild.github.io/api/#splitting
|
||||
- There is a known bug with ESBuild that loads chunks out of order: https://github.com/evanw/esbuild/issues/399
|
||||
- The complete client bundle is currently only ~10kB transferred; splitting this down further is unlikely to result in significant load speed gains
|
||||
- A ticket exists (MNTOR-1171) to set up logical code-splitting depending on partial. Once complete, the conflict/redundancy with ESBuild code-splitting will increase
|
||||
- A ticket was completed (MNTOR-1171) to set up native/logical code-splitting depending on partial.
|
||||
- see also https://github.com/mozilla/blurts-server/pull/2844
|
||||
*/
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
196
package.json
196
package.json
|
@ -1,136 +1,96 @@
|
|||
{
|
||||
"name": "blurts-server",
|
||||
"description": "Firefox Monitor Server",
|
||||
"name": "monitor",
|
||||
"version": "1.0.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mozilla/blurts-server/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluent/bundle": "^0.17.1",
|
||||
"@fluent/langneg": "^0.6.2",
|
||||
"@maxmind/geoip2-node": "^3.1.0",
|
||||
"@sentry/node": "7.36.0",
|
||||
"@sentry/tracing": "^7.36.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"client-oauth2": "4.3.3",
|
||||
"connect-redis": "^7.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csurf": "1.11.0",
|
||||
"dotenv": "16.0.2",
|
||||
"esbuild": "^0.17.8",
|
||||
"express": "^4.18.2",
|
||||
"express-bearer-token": "2.4.0",
|
||||
"express-handlebars": "6.0.6",
|
||||
"express-session": "1.17.1",
|
||||
"fluent": "0.13.0",
|
||||
"fluent-langneg": "0.2.0",
|
||||
"git-rev-sync": "^3.0.2",
|
||||
"got": "11.8.5",
|
||||
"helmet": "4.2.0",
|
||||
"intl-pluralrules": "1.3.1",
|
||||
"isemail": "3.2.0",
|
||||
"knex": "^2.2.0",
|
||||
"mozlog": "3.0.2",
|
||||
"nodemailer": "^6.7.8",
|
||||
"nodemailer-express-handlebars": "5.0.0",
|
||||
"pg": "^8.7.1",
|
||||
"redis": "^4.6.5",
|
||||
"sns-validator": "0.3.5",
|
||||
"uuid": "3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/gtag.js": "^0.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||
"@typescript-eslint/parser": "^5.57.0",
|
||||
"coveralls": "3.1.1",
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.2.1",
|
||||
"eslint-plugin-promise": "^6.0.1",
|
||||
"jest": "^27.2.5",
|
||||
"node-mocks-http": "1.11.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"redis-mock": "^0.56.3",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"description": "Monitor V2",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "18.12.x",
|
||||
"npm": "8.x"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/blurts-server",
|
||||
"license": "MPL-2.0",
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"**/*.js",
|
||||
"!coverage/**/**.js",
|
||||
"!db/seeds/**.js",
|
||||
"!db/migrations/**.js",
|
||||
"!public/**/**.js",
|
||||
"!scripts/*.js",
|
||||
"!loadtests/**/**.js",
|
||||
"!.eslintrc.js"
|
||||
"scripts": {
|
||||
"prestart": "npm run build",
|
||||
"start": "node src/app.js",
|
||||
"dev": "nodemon src/app.js",
|
||||
"build": "node esbuild & npm run copy:root & npm run copy:webp & npm run copy:png & npm run build:svg",
|
||||
"build:svg": "svgo -f src/client/images/ -r -o dist/images",
|
||||
"copy:root": "mkdir -p dist/ && cp src/client/*.* dist/",
|
||||
"copy:webp": "mkdir -p dist/images/ && cp -r src/client/images/*.webp dist/images/",
|
||||
"copy:png": "mkdir -p dist/images/email/ && cp -r src/client/images/email/*.png dist/images/email/",
|
||||
"convert:webp": "sh src/scripts/webp.sh",
|
||||
"db:migrate": "node -r dotenv/config node_modules/knex/bin/cli.js migrate:latest --knexfile src/db/knexfile.js",
|
||||
"db:rollback": "node -r dotenv/config node_modules/knex/bin/cli.js migrate:rollback --knexfile src/db/knexfile.js",
|
||||
"test": "NODE_OPTIONS=--loader=testdouble c8 ava",
|
||||
"e2e": "playwright test src/e2e/",
|
||||
"lint": "npm run lint:css && npm run lint:js",
|
||||
"lint:js": "eslint .",
|
||||
"lint:css": "stylelint src/client/css/",
|
||||
"fix": "npm run fix:css && npm run fix:js",
|
||||
"fix:js": "eslint . --fix",
|
||||
"fix:css": "stylelint src/client/css/ --fix"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
"*",
|
||||
".env"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"./tests/jest.setup.js"
|
||||
"ignore": [
|
||||
"src/client/*"
|
||||
],
|
||||
"env": {
|
||||
"LIVE_RELOAD": true
|
||||
},
|
||||
"ext": "js,css,json,ftl,env"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
"!e2e/"
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mozilla/blurts-server.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "npm start -w src",
|
||||
"start:old": "npm run build && node server.js",
|
||||
"dev": "npm run dev -w src",
|
||||
"dev:old": "nodemon server.js",
|
||||
"build": "node esbuild.js",
|
||||
"db:migrate": "knex migrate:latest --knexfile db/knexfile.js",
|
||||
"db:rollback": "knex migrate:rollback --knexfile db/knexfile.js",
|
||||
"docker:build": "docker build -t blurts-server .",
|
||||
"docker:run": "docker run -p 6060:6060 blurts-server",
|
||||
"lint": "npm run lint:css && npm run lint:js",
|
||||
"lint:js": "eslint src",
|
||||
"lint:ts": "tsc --noEmit",
|
||||
"lint:css": "stylelint public/css/",
|
||||
"lint:fluent": "moz-fluent-lint ./locales/en --config .github/linter_config.yml",
|
||||
"fix": "npm run fix:css && npm run fix:js",
|
||||
"fix:js": "eslint . --fix",
|
||||
"fix:css": "stylelint public/css/ --fix",
|
||||
"test:db:migrate": "NODE_ENV=tests knex migrate:latest --knexfile db/knexfile.js --env tests",
|
||||
"test:tests": "NODE_ENV=tests HIBP_THROTTLE_DELAY=1000 HIBP_THROTTLE_MAX_TRIES=3 jest --runInBand --coverage tests/",
|
||||
"test:coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"test:integration": "wdio tests/integration/wdio.conf.js",
|
||||
"test:integration-headless": "MOZ_HEADLESS=1 wdio tests/integration/wdio.conf.js",
|
||||
"test:integration-headless-ci": "MOZ_HEADLESS=1 ERROR_SHOTS=1 wdio tests/integration/wdio.conf.js",
|
||||
"test:integration-docker": "MOZ_HEADLESS=1 wdio tests/integration/wdio.docker.js",
|
||||
"test": "npm run test:db:migrate && npm run test:tests && (node scripts/is-coveralls-configured.js && npm run test:coveralls || echo 'Skipping coveralls.')"
|
||||
},
|
||||
"workspaces": [
|
||||
"src"
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"events": {
|
||||
"restart": "npm run build"
|
||||
},
|
||||
"watch": [
|
||||
"./*",
|
||||
".env",
|
||||
".env-dist"
|
||||
],
|
||||
"ignore": [
|
||||
"public/dist/*",
|
||||
"tests/*"
|
||||
],
|
||||
"ext": "js,css,hbs,json,ftl"
|
||||
},
|
||||
"supportedLocales": "cak,cs,cy,da,de,el,en,en-CA,en-GB,es-AR,es-CL,es-ES,es-MX,fi,fr,fy-NL,gn,hu,kab,ia,id,it,ja,nb-NO,nl,nn-NO,pt-BR,pt-PT,ro,ru,sk,sl,sq,sv-SE,tr,uk,vi,zh-CN,zh-TW",
|
||||
"homepage": "https://github.com/mozilla/blurts-server",
|
||||
"license": "MPL-2.0",
|
||||
"volta": {
|
||||
"node": "18.12.1",
|
||||
"npm": "8.19.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluent/bundle": "^0.17.1",
|
||||
"@fluent/langneg": "^0.6.2",
|
||||
"@sentry/node": "^7.40.0",
|
||||
"@sentry/tracing": "^7.38.0",
|
||||
"client-oauth2": "^4.3.3",
|
||||
"connect-redis": "^7.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csrf-csrf": "^2.2.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"esbuild": "^0.17.8",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-session": "^1.17.3",
|
||||
"helmet": "^6.0.0",
|
||||
"knex": "^2.4.2",
|
||||
"mozlog": "^3.0.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"pg": "^8.9.0",
|
||||
"redis": "^4.6.5",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.30.0",
|
||||
"ava": "^5.1.0",
|
||||
"c8": "^7.12.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-check-file": "^2.2.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"redis-mock": "^0.56.3",
|
||||
"svgo": "^3.0.2",
|
||||
"testdouble": "^3.16.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ Sentry.init({
|
|||
// Build script is triggered for `npm start` and assets are served from /dist.
|
||||
// Build script is NOT run for `npm run dev`, assets are served from /src, and nodemon restarts server without build (faster dev).
|
||||
const staticPath =
|
||||
process.env.npm_lifecycle_event === 'start' ? '../dist' : './client'
|
||||
process.env.npm_lifecycle_event === 'start' ? 'dist' : 'src/client'
|
||||
|
||||
await initFluentBundles()
|
||||
|
||||
|
@ -198,7 +198,7 @@ app.use(errorHandler)
|
|||
|
||||
app.listen(AppConstants.PORT, async function () {
|
||||
console.info(`MONITOR V2: Server listening at ${this.address().port}`)
|
||||
console.info(`Static files served from ${staticPath}`)
|
||||
console.info(`Client files served from ${staticPath}`)
|
||||
try {
|
||||
await initEmail()
|
||||
console.info('Email initialized')
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
|
||||
// TODO: these vars were copy/pasted from the old app-constants.js and should be cleaned up
|
||||
import * as dotenv from 'dotenv'
|
||||
|
||||
dotenv.config({ path: '../.env' })
|
||||
dotenv.config()
|
||||
|
||||
const requiredEnvVars = [
|
||||
'ADMINS',
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
{
|
||||
"name": "monitor",
|
||||
"version": "1.0.0",
|
||||
"description": "Monitor V2",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "18.12.x",
|
||||
"npm": "8.x"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "npm run build",
|
||||
"start": "node app.js",
|
||||
"dev": "nodemon app.js",
|
||||
"build": "node esbuild & npm run copy:root & npm run copy:webp & npm run copy:png & npm run build:svg",
|
||||
"build:svg": "svgo -f ./client/images/ -r -o ../dist/images",
|
||||
"copy:root": "mkdir -p ../dist/ && cp ./client/*.* ../dist/",
|
||||
"copy:webp": "mkdir -p ../dist/images/ && cp -r ./client/images/*.webp ../dist/images/",
|
||||
"copy:png": "mkdir -p ../dist/images/email/ && cp -r ./client/images/email/*.png ../dist/images/email/",
|
||||
"convert:webp": "sh ./scripts/webp.sh",
|
||||
"test": "NODE_OPTIONS=--loader=testdouble c8 ava",
|
||||
"e2e": "playwright test e2e/",
|
||||
"lint": "npm run lint:css && npm run lint:js",
|
||||
"lint:js": "eslint .",
|
||||
"lint:css": "stylelint client/css/",
|
||||
"fix": "npm run fix:css && npm run fix:js",
|
||||
"fix:js": "eslint . --fix",
|
||||
"fix:css": "stylelint client/css/ --fix"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
"./*",
|
||||
"../.env",
|
||||
"../.env-dist",
|
||||
"../locales/en/*"
|
||||
],
|
||||
"ignore": [
|
||||
"src/client/*"
|
||||
],
|
||||
"env": {
|
||||
"LIVE_RELOAD": true
|
||||
},
|
||||
"ext": "js,css,json,ftl"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
"!e2e/"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mozilla/blurts-server.git"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/blurts-server",
|
||||
"license": "MPL-2.0",
|
||||
"volta": {
|
||||
"node": "18.12.1",
|
||||
"npm": "8.19.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluent/bundle": "^0.17.1",
|
||||
"@fluent/langneg": "^0.6.2",
|
||||
"@sentry/node": "^7.40.0",
|
||||
"@sentry/tracing": "^7.38.0",
|
||||
"client-oauth2": "^4.3.3",
|
||||
"connect-redis": "^7.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csrf-csrf": "^2.2.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"esbuild": "^0.17.8",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-session": "^1.17.3",
|
||||
"helmet": "^6.0.0",
|
||||
"knex": "^2.4.2",
|
||||
"mozlog": "^3.0.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"pg": "^8.9.0",
|
||||
"redis": "^4.6.5",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.30.0",
|
||||
"ava": "^5.1.0",
|
||||
"c8": "^7.12.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-check-file": "^2.2.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"redis-mock": "^0.56.3",
|
||||
"svgo": "^3.0.2",
|
||||
"testdouble": "^3.16.8"
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ const fluentBundles = {}
|
|||
async function initFluentBundles () {
|
||||
const promises = supportedLocales.map(async locale => {
|
||||
const bundle = new FluentBundle(locale, { useIsolating: false })
|
||||
const dirname = resolve('../locales', locale)
|
||||
const dirname = resolve('./locales', locale)
|
||||
|
||||
try {
|
||||
const filenames = readdirSync(dirname).filter(item => item.endsWith('.ftl'))
|
||||
|
|
Загрузка…
Ссылка в новой задаче