diff --git a/Makefile b/Makefile index 3f11bc8165..8a25863424 100644 --- a/Makefile +++ b/Makefile @@ -231,7 +231,7 @@ i18n-pretranslate-approve-all: i18n-download-translations: python packages/kolibri-tools/lib/i18n/crowdin.py rebuild-translations ${branch} python packages/kolibri-tools/lib/i18n/crowdin.py download-translations ${branch} - yarn exec kolibri-tools i18n-code-gen -- --output-dir ./packages/kolibri/utils + yarn exec kolibri-tools i18n-code-gen -- --output-dir ./packages/kolibri/utils/internal $(MAKE) i18n-django-compilemessages yarn exec kolibri-tools i18n-create-message-files -- --pluginFile ./build_tools/build_plugins.txt diff --git a/packages/kolibri-tools/lib/apiSpecExportTools.js b/packages/kolibri-tools/lib/apiSpecExportTools.js index 56a4480ae1..fb6c7ed832 100644 --- a/packages/kolibri-tools/lib/apiSpecExportTools.js +++ b/packages/kolibri-tools/lib/apiSpecExportTools.js @@ -1,22 +1,29 @@ const path = require('node:path'); const kolibriPackageJson = require('../../kolibri/package.json'); const writeSourceToFile = require('./i18n/writeSourceToFile'); +const glob = require('./glob'); const apiSpec = kolibriPackageJson.exports || {}; const { kolibriName } = require('./kolibriName'); -// Generate a list of all the module imports that we need to expose -// Iterate over all the exports in the kolibri package -const apiKeys = Object.keys(apiSpec) - // Filter out the export for the root package '.' as we don't need to expose that - .filter(key => key !== '.') - // Add the kolibri prefix and remove the leading '.' to make a full import path - // e.g. './urls' -> 'kolibri/urls' - .map(key => 'kolibri' + key.slice(1)) - // Add the list of modules that are exposed in the kolibri package.json - // Unmodified, as they are already full import paths, e.g. 'vue' - .concat(kolibriPackageJson.exposes); +function generateApiKeys(apiSpec) { + // Generate a list of all the module imports that we need to expose + // Iterate over all the exports in the kolibri package + return ( + Object.keys(apiSpec) + // Filter out the export for the root package '.' as we don't need to expose that + .filter(key => key !== '.') + // Add the kolibri prefix and remove the leading '.' to make a full import path + // e.g. './urls' -> 'kolibri/urls' + .map(key => 'kolibri' + key.slice(1)) + // Add the list of modules that are exposed in the kolibri package.json + // Unmodified, as they are already full import paths, e.g. 'vue' + .concat(kolibriPackageJson.exposes) + ); +} + +const apiKeys = generateApiKeys(apiSpec); const coreExternals = { // The kolibri package itself is a special case, as it is the root of the package @@ -38,10 +45,36 @@ const apiSpecHeader = ` `; function rebuildApiSpec() { - const apiSpecFilePath = path.resolve(__dirname, '../../kolibri/apiSpec.js'); + // First we read the directory structure of the kolibri folder to infer the list of modules + // that are available to be imported. + const kolibriFolder = path.resolve(__dirname, '../../kolibri'); + const kolibriFiles = glob + .sync(`${kolibriFolder}/**/*.{js,vue}`, { + ignore: ['**/internal/**', '**/__tests__/**', '**/__mocks__/**'], + }) + .map(f => f.split('.')[0]) + .map(f => f.replace(kolibriFolder, '')) + .map(f => f.replace(/\/index$/, '')) + .sort(); + // Then we generate the list of modules that are exposed in the kolibri package.json + const newApiSpec = {}; + for (const key of kolibriFiles) { + const specValue = '.' + key; + // When we are able to move everything into a src folder + // we can update the right hand side of this + newApiSpec[specValue] = specValue === '.' ? './index' : specValue; + } + const updatedKolibriPackageJson = { + ...kolibriPackageJson, + exports: newApiSpec, + }; + const kolibriPackageJsonFilePath = path.resolve(__dirname, '../../kolibri/package.json'); + writeSourceToFile(kolibriPackageJsonFilePath, JSON.stringify(updatedKolibriPackageJson, null, 2)); + const apiSpecFilePath = path.resolve(__dirname, '../../kolibri/internal/apiSpec.js'); + const updatedApiKeys = generateApiKeys(updatedKolibriPackageJson.exports); let apiSpecContent = apiSpecHeader; apiSpecContent += 'export default {\n'; - for (const key of apiKeys) { + for (const key of updatedApiKeys) { apiSpecContent += ` '${key}': require('${key}'),\n`; } apiSpecContent += '};\n'; diff --git a/packages/kolibri/README.md b/packages/kolibri/README.md new file mode 100644 index 0000000000..b807d73cb0 --- /dev/null +++ b/packages/kolibri/README.md @@ -0,0 +1,3 @@ +This is the kolibri JS public API. All the exports and 'exposes' fields in the package.json are available at runtime as part of the Kolibri core bundle, so JS built with the Kolibri webpack configuration will defer to a global object to read these modules at runtime. + +Only the files detailed in 'exports' are available for import from this package, as per ESM conventions - to further clarify which modules are importable, all the code that is only used internally within this package is included inside folders called 'internal'. Any code inside these folders will not be directly importable. diff --git a/packages/kolibri/__tests__/heartbeat.spec.js b/packages/kolibri/__tests__/heartbeat.spec.js index ce1ee648d3..1e2a286d56 100644 --- a/packages/kolibri/__tests__/heartbeat.spec.js +++ b/packages/kolibri/__tests__/heartbeat.spec.js @@ -7,7 +7,7 @@ import useSnackbar, { useSnackbarMock } from 'kolibri/composables/useSnackbar'; import { ref } from '@vue/composition-api'; import { DisconnectionErrorCodes } from 'kolibri/constants'; import { HeartBeat } from '../heartbeat.js'; -import { trs } from '../disconnection'; +import { trs } from '../internal/disconnection'; import coreModule from '../../../kolibri/core/assets/src/state/modules/core'; import { stubWindowLocation } from 'testUtils'; // eslint-disable-line diff --git a/packages/kolibri/client.js b/packages/kolibri/client.js index 8ba6f0db88..5a319f3917 100644 --- a/packages/kolibri/client.js +++ b/packages/kolibri/client.js @@ -10,7 +10,7 @@ import { get } from '@vueuse/core'; import useUser from 'kolibri/composables/useUser'; import { DisconnectionErrorCodes } from 'kolibri/constants'; import clientFactory from 'kolibri/utils/baseClient'; -import useConnection from './useConnection'; +import useConnection from './internal/useConnection'; export const logging = logger.getLogger(__filename); diff --git a/packages/kolibri/components/ContentRenderer/ContentRendererError.vue b/packages/kolibri/components/internal/ContentRenderer/ContentRendererError.vue similarity index 100% rename from packages/kolibri/components/ContentRenderer/ContentRendererError.vue rename to packages/kolibri/components/internal/ContentRenderer/ContentRendererError.vue diff --git a/packages/kolibri/components/ContentRenderer/ContentRendererLoading.vue b/packages/kolibri/components/internal/ContentRenderer/ContentRendererLoading.vue similarity index 100% rename from packages/kolibri/components/ContentRenderer/ContentRendererLoading.vue rename to packages/kolibri/components/internal/ContentRenderer/ContentRendererLoading.vue diff --git a/packages/kolibri/components/ContentRenderer/__tests__/ContentRendererError.spec.js b/packages/kolibri/components/internal/ContentRenderer/__tests__/ContentRendererError.spec.js similarity index 100% rename from packages/kolibri/components/ContentRenderer/__tests__/ContentRendererError.spec.js rename to packages/kolibri/components/internal/ContentRenderer/__tests__/ContentRendererError.spec.js diff --git a/packages/kolibri/components/ContentRenderer/__tests__/ContentRendererLoading.spec.js b/packages/kolibri/components/internal/ContentRenderer/__tests__/ContentRendererLoading.spec.js similarity index 100% rename from packages/kolibri/components/ContentRenderer/__tests__/ContentRendererLoading.spec.js rename to packages/kolibri/components/internal/ContentRenderer/__tests__/ContentRendererLoading.spec.js diff --git a/packages/kolibri/components/ContentRenderer/index.js b/packages/kolibri/components/internal/ContentRenderer/index.js similarity index 100% rename from packages/kolibri/components/ContentRenderer/index.js rename to packages/kolibri/components/internal/ContentRenderer/index.js diff --git a/packages/kolibri/components/ContentRenderer/mixin.js b/packages/kolibri/components/internal/ContentRenderer/mixin.js similarity index 100% rename from packages/kolibri/components/ContentRenderer/mixin.js rename to packages/kolibri/components/internal/ContentRenderer/mixin.js diff --git a/packages/kolibri/components/language-switcher/LanguageSwitcherList.vue b/packages/kolibri/components/language-switcher/LanguageSwitcherList.vue index 16cb4d931b..b46f896ddb 100644 --- a/packages/kolibri/components/language-switcher/LanguageSwitcherList.vue +++ b/packages/kolibri/components/language-switcher/LanguageSwitcherList.vue @@ -55,8 +55,8 @@ import { availableLanguages, compareLanguages, currentLanguage } from 'kolibri/utils/i18n'; import LanguageSwitcherModal from 'kolibri/components/language-switcher/LanguageSwitcherModal'; - import languageSwitcherMixin from './mixin'; - import SelectedLanguage from './SelectedLanguage'; + import languageSwitcherMixin from './internal/mixin'; + import SelectedLanguage from './internal/SelectedLanguage'; const prioritizedLanguages = ['en', 'ar', 'es-419', 'hi-in', 'fr-fr', 'sw-tz']; @@ -118,7 +118,7 @@