feat: ported xml-namespace-loader

This commit is contained in:
Igor Randjelovic
2020-11-28 21:26:16 +01:00
parent 803958266d
commit 5b182c0d5f
7 changed files with 699 additions and 9 deletions

View File

@@ -16,6 +16,7 @@ import {
getEntryPath,
getPlatform,
} from '../helpers/project';
import { hasDependency } from '../helpers/dependencies';
export default function (config: Config, env: IWebpackEnv): Config {
const entryPath = getEntryPath();
@@ -136,13 +137,17 @@ export default function (config: Config, env: IWebpackEnv): Config {
});
// Use Fork TS Checker to do type checking in a separate non-blocking process
config.plugin('ForkTsCheckerWebpackPlugin').use(ForkTsCheckerWebpackPlugin, [
{
typescript: {
memoryLimit: 4096,
},
},
]);
config.when(hasDependency('typescript'), (config) => {
config
.plugin('ForkTsCheckerWebpackPlugin')
.use(ForkTsCheckerWebpackPlugin, [
{
typescript: {
memoryLimit: 4096,
},
},
]);
});
// set up js
// todo: do we need babel-loader? It's useful to support it

View File

@@ -1,18 +1,37 @@
import VirtualModulesPlugin from 'webpack-virtual-modules';
import Config from 'webpack-chain';
import { getEntryPath } from '../helpers/project';
import { IWebpackEnv } from '../index';
import base from './base';
import dedent from 'ts-dedent';
// todo: add base configuration for core with javascript
export default function (config: Config, env: IWebpackEnv): Config {
base(config, env);
const virtualEntryPath = getEntryPath() + '.virtual.js';
config.entry('bundle').add(virtualEntryPath);
// Add a virtual entry module
config.plugin('VirtualModulesPlugin').use(VirtualModulesPlugin, [
{
[virtualEntryPath]: dedent`
require('@nativescript/core/bundle-entry-points')
const context = require.context("./", /* deep: */ true);
global.registerWebpackModules(context);
`,
},
]);
// set up xml
config.module
.rule('xml')
.test(/\.xml$/)
.use('xml-loader')
.loader('xml-loader');
.use('xml-namespace-loader')
.loader('xml-namespace-loader');
return config;
}

View File

@@ -1,6 +1,7 @@
import { getPackageJson, getProjectRootPath } from './project';
import path from 'path';
// todo: memoize
export function getAllDependencies(): string[] {
const packageJSON = getPackageJson();
@@ -10,6 +11,12 @@ export function getAllDependencies(): string[] {
];
}
// todo: memoize
export function hasDependency(dependencyName: string) {
return getAllDependencies().includes(dependencyName);
}
// todo: memoize
export function getDependencyPath(dependencyName: string): string | null {
try {
const resolvedPath = require.resolve(`${dependencyName}/package.json`, {

View File

@@ -0,0 +1,228 @@
import { parse, join } from 'path';
import { promisify } from 'util';
import dedent from 'ts-dedent';
import { parser } from 'sax';
const noop = () => {};
const DEBUG = false;
interface NamespaceEntry {
name: string;
path: string;
}
interface ParseResult {
code: string;
}
export default function loader(content: string, map: any) {
const callback = this.async();
// parse content and dependencies async
parseXML
.bind(this)(content)
.then((res) => {
DEBUG && console.log({ res });
callback(null, res.code, map);
})
.catch((err) => {
DEBUG && console.log({ err });
callback(err);
});
}
async function parseXML(content: string): Promise<ParseResult> {
// wrap this.resolve into a promise
const resolveAsync = promisify(this.resolve);
const promises: Promise<any>[] = [];
const namespaces: NamespaceEntry[] = [];
const distinctNamespaces = new Map<string, string>();
const moduleRegisters: string[] = [];
const { ignore } = this.query;
const errors = [];
const saxParser = parser(true, { xmlns: true });
// // Register ios and android prefixes as namespaces to avoid "unbound xml namespace" errors
(saxParser as any).ns['ios'] = 'http://schemas.nativescript.org/tns.xsd';
(saxParser as any).ns['android'] = 'http://schemas.nativescript.org/tns.xsd';
(saxParser as any).ns['desktop'] = 'http://schemas.nativescript.org/tns.xsd';
(saxParser as any).ns['web'] = 'http://schemas.nativescript.org/tns.xsd';
const handleOpenTag = async (namespace: string, elementName: string) => {
if (!namespace) {
return;
}
if (namespace.startsWith('http')) {
return;
}
const moduleName = `${namespace}/${elementName}`;
if (namespaces.some((n) => n.name === moduleName)) {
return;
}
if (ignore && moduleName.match(ignore)) {
return;
}
const localNamespacePath = join(this.rootContext, namespace);
const localModulePath = join(localNamespacePath, elementName);
const resolvePaths = [
localNamespacePath,
localModulePath,
`${localModulePath}.xml`,
moduleName,
namespace,
];
DEBUG && console.log({ resolvePaths });
let resolvedPath;
for (const p of resolvePaths) {
resolvedPath = await resolveAsync(this.context, p).catch(noop);
// break on first match
if (resolvedPath) {
break;
}
}
DEBUG && console.log({ resolvedPath });
// bail if we haven't resolved a path
if (!resolvedPath) {
return;
}
const { dir, name } = parse(resolvedPath);
// register resolved path + short name
namespaces.push({ name: namespace, path: resolvedPath });
namespaces.push({ name: moduleName, path: resolvedPath });
this.addDependency(resolvedPath);
const noExtFilename = join(dir, name);
DEBUG &&
console.log({
noExtFilename,
});
// finally try resolving an XML file
await resolveAsync(this.context, `${noExtFilename}.xml`)
.then((xml) => {
this.addDependency(xml);
namespaces.push({ name: `${moduleName}.xml`, path: xml });
})
.catch(() => {
// if there is no XML file, fall back to namespace as the path
// will become require(<namespace>)
namespaces.push({ name: namespace, path: namespace });
namespaces.push({ name: moduleName, path: namespace });
});
// look for css files with the same name
await resolveAsync(this.context, `${noExtFilename}.css`)
.then((css) => {
this.addDependency(css);
// namespaces.push({ name: `${moduleName}.css`, path: css });
})
.catch(noop);
};
saxParser.onopentag = (node) => {
if ('uri' in node) {
promises.push(handleOpenTag(node.uri, node.local));
}
};
saxParser.onerror = (error) => {
saxParser.error = null;
// Do only warning about invalid character "&"" for back-compatibility
// as it is common to use it in a binding expression
if (
error.message.includes('Invalid character') &&
error.message.includes('Char: &')
) {
return this.emitWarning(error);
}
errors.push(error);
};
saxParser.write(content).close();
await Promise.all(promises);
DEBUG && console.log({ namespaces });
namespaces.forEach(({ name, path }) => {
distinctNamespaces.set(name, path.replace(/\\/g, '/'));
});
distinctNamespaces.forEach((path, name) => {
moduleRegisters.push(dedent`
global.registerModule(
'${name}',
() => require("${path}")
)
`);
});
// escape special whitespace characters
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Issue_with_plain_JSON.stringify_for_use_as_JavaScript
const xml = JSON.stringify(content)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
const code = dedent`
${moduleRegisters.join('\n')}
/* XML-NAMESPACE-LOADER */
const ___XML_NAMESPACE_LOADER_EXPORT___ = ${xml}
export default ___XML_NAMESPACE_LOADER_EXPORT___
`;
if (errors.length) {
errors.map(this.emitError);
// finally throw the first one
throw errors[0];
}
return {
code,
};
}
//
//
//
// function parseXML(xml: string) {
// const saxParser = parser(true, { xmlns: true });
//
// saxParser.onopentag = (node) => {
// if('ns' in node) {
// const uri = node.uri
// const tag = node.local
//
// DEBUG && console.log({
// uri,
// tag
// })
// }
// }
//
// saxParser.onerror = (err) => {
// DEBUG && console.log(err)
// }
//
// // Register ios and android prefixes as namespaces to avoid "unbound xml namespace" errors
// // saxParser.ns['ios'] = 'http://schemas.nativescript.org/tns.xsd';
// // saxParser.ns['android'] = 'http://schemas.nativescript.org/tns.xsd';
// // saxParser.ns['desktop'] = 'http://schemas.nativescript.org/tns.xsd';
// // saxParser.ns['web'] = 'http://schemas.nativescript.org/tns.xsd';
// saxParser.write(xml).close()
// }